Compare commits
44 Commits
d6cf736abf
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
e6967b8bbb
|
|||
|
d2f9a9b83b
|
|||
|
1b5ecd9eaf
|
|||
|
82561d62b6
|
|||
|
eec021cc4b
|
|||
|
a1d98823f8
|
|||
|
255b77d91d
|
|||
|
f84ec5a3f8
|
|||
|
eb22a8bcc1
|
|||
|
31aef905fa
|
|||
|
a6887f7253
|
|||
|
69bd581af7
|
|||
|
26b7afc890
|
|||
|
d5532aade0
|
|||
|
0c5409aec7
|
|||
|
1a8840bebc
|
|||
|
1fb453dffe
|
|||
|
e03d702d08
|
|||
|
241dc964a6
|
|||
|
8ef71e14d5
|
|||
|
972f4006f0
|
|||
|
9a8a047908
|
|||
|
863bf69ad3
|
|||
|
0e957cc9c1
|
|||
|
aa454b158f
|
|||
|
7007bd6a1c
|
|||
|
00efc95ee7
|
|||
|
b380bb248c
|
|||
|
87e008d56d
|
|||
|
3992073212
|
|||
|
ef80b19f2f
|
|||
|
717771ae80
|
|||
|
bf5772bd8a
|
|||
|
9a7c81a44e
|
|||
|
b7e991de5b
|
|||
|
6c1205106d
|
|||
|
2ffca6984a
|
|||
|
dde2516304
|
|||
|
f30a439bcd
|
|||
|
008e9e7fc5
|
|||
|
23aefcd759
|
|||
|
cb8b886446
|
|||
|
5979d8b1e0
|
|||
|
e587112e63
|
@@ -20,5 +20,5 @@ jobs:
|
||||
uses: https://gitea.com/actions/release-action@main
|
||||
with:
|
||||
files: |-
|
||||
result/fortify-**
|
||||
result/hakurei-**
|
||||
api_key: '${{secrets.RELEASE_TOKEN}}'
|
||||
|
||||
@@ -5,25 +5,25 @@ on:
|
||||
- pull_request
|
||||
|
||||
jobs:
|
||||
fortify:
|
||||
name: Fortify
|
||||
hakurei:
|
||||
name: Hakurei
|
||||
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.fortify
|
||||
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.hakurei
|
||||
|
||||
- name: Upload test output
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "fortify-vm-output"
|
||||
name: "hakurei-vm-output"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
race:
|
||||
name: Fortify (race detector)
|
||||
name: Hakurei (race detector)
|
||||
runs-on: nix
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
- name: Upload test output
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "fortify-race-vm-output"
|
||||
name: "hakurei-race-vm-output"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
@@ -73,31 +73,31 @@ jobs:
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
fpkg:
|
||||
name: Fpkg
|
||||
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.fpkg
|
||||
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: "fpkg-vm-output"
|
||||
name: "planterette-vm-output"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
check:
|
||||
name: Flake checks
|
||||
needs:
|
||||
- fortify
|
||||
- hakurei
|
||||
- race
|
||||
- sandbox
|
||||
- sandbox-race
|
||||
- fpkg
|
||||
- planterette
|
||||
runs-on: nix
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -116,15 +116,15 @@ jobs:
|
||||
- name: Build for test
|
||||
id: build-test
|
||||
run: >-
|
||||
export FORTIFY_REV="$(git rev-parse --short HEAD)" &&
|
||||
sed -i.old 's/version = /version = "0.0.0-'$FORTIFY_REV'"; # version = /' package.nix &&
|
||||
export HAKUREI_REV="$(git rev-parse --short HEAD)" &&
|
||||
sed -i.old 's/version = /version = "0.0.0-'$HAKUREI_REV'"; # version = /' package.nix &&
|
||||
nix build --print-out-paths --print-build-logs .#dist &&
|
||||
mv package.nix.old package.nix &&
|
||||
echo "rev=$FORTIFY_REV" >> $GITHUB_OUTPUT
|
||||
echo "rev=$HAKUREI_REV" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload test build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "fortify-${{ steps.build-test.outputs.rev }}"
|
||||
name: "hakurei-${{ steps.build-test.outputs.rev }}"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
1
.github/workflows/README
vendored
Normal file
1
.github/workflows/README
vendored
Normal file
@@ -0,0 +1 @@
|
||||
This port is solely for releasing to the github mirror and serves no purpose during development.
|
||||
46
.github/workflows/release.yml
vendored
Normal file
46
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Create release
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Nix
|
||||
uses: nixbuild/nix-quick-install-action@v32
|
||||
with:
|
||||
nix_conf: |
|
||||
keep-env-derivations = true
|
||||
keep-outputs = true
|
||||
|
||||
- name: Restore and cache Nix store
|
||||
uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: build-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
|
||||
restore-prefixes-first-match: build-${{ runner.os }}-
|
||||
gc-max-store-size-linux: 1G
|
||||
purge: true
|
||||
purge-prefixes: build-${{ runner.os }}-
|
||||
purge-created: 60
|
||||
purge-primary-key: never
|
||||
|
||||
- name: Build for release
|
||||
run: nix build --print-out-paths --print-build-logs .#dist
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |-
|
||||
result/hakurei-**
|
||||
48
.github/workflows/test.yml
vendored
Normal file
48
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
- push
|
||||
|
||||
jobs:
|
||||
dist:
|
||||
name: Create distribution
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Nix
|
||||
uses: nixbuild/nix-quick-install-action@v32
|
||||
with:
|
||||
nix_conf: |
|
||||
keep-env-derivations = true
|
||||
keep-outputs = true
|
||||
|
||||
- name: Restore and cache Nix store
|
||||
uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: build-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
|
||||
restore-prefixes-first-match: build-${{ runner.os }}-
|
||||
gc-max-store-size-linux: 1G
|
||||
purge: true
|
||||
purge-prefixes: build-${{ runner.os }}-
|
||||
purge-created: 60
|
||||
purge-primary-key: never
|
||||
|
||||
- name: Build for test
|
||||
id: build-test
|
||||
run: >-
|
||||
export HAKUREI_REV="$(git rev-parse --short HEAD)" &&
|
||||
sed -i.old 's/version = /version = "0.0.0-'$HAKUREI_REV'"; # version = /' package.nix &&
|
||||
nix build --print-out-paths --print-build-logs .#dist &&
|
||||
mv package.nix.old package.nix &&
|
||||
echo "rev=$HAKUREI_REV" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload test build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: "hakurei-${{ steps.build-test.outputs.rev }}"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -5,7 +5,7 @@
|
||||
*.so
|
||||
*.dylib
|
||||
*.pkg
|
||||
/fortify
|
||||
/hakurei
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
@@ -26,7 +26,7 @@ go.work.sum
|
||||
.vscode
|
||||
|
||||
# go generate
|
||||
security-context-v1-protocol.*
|
||||
/cmd/hakurei/LICENSE
|
||||
|
||||
# release
|
||||
/dist/fortify-*
|
||||
/dist/hakurei-*
|
||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2024 Ophestra Umiker
|
||||
Copyright (c) 2024-2025 Ophestra
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
105
README.md
105
README.md
@@ -1,77 +1,79 @@
|
||||
Fortify
|
||||
=======
|
||||
<p align="center">
|
||||
<a href="https://git.gensokyo.uk/security/hakurei">
|
||||
<picture>
|
||||
<img src="https://basement.gensokyo.uk/images/yukari1.png" width="200px" alt="Yukari">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[](https://pkg.go.dev/git.gensokyo.uk/security/fortify)
|
||||
[](https://goreportcard.com/report/git.gensokyo.uk/security/fortify)
|
||||
<p align="center">
|
||||
<a href="https://pkg.go.dev/hakurei.app"><img src="https://pkg.go.dev/badge/hakurei.app.svg" alt="Go Reference" /></a>
|
||||
<a href="https://goreportcard.com/report/hakurei.app"><img src="https://goreportcard.com/badge/hakurei.app" alt="Go Report Card" /></a>
|
||||
</p>
|
||||
|
||||
Lets you run graphical applications as another user in a confined environment with a nice NixOS
|
||||
module to configure target users and provide launchers and desktop files for your privileged user.
|
||||
Hakurei is a tool for running sandboxed graphical applications as dedicated subordinate users on the Linux kernel.
|
||||
It also implements [planterette (WIP)](cmd/planterette), a self-contained Android-like package manager with modern security features.
|
||||
|
||||
Why would you want this?
|
||||
## NixOS Module usage
|
||||
|
||||
- It protects the desktop environment from applications.
|
||||
|
||||
- It protects applications from each other.
|
||||
|
||||
- It provides UID isolation on top of the standard application sandbox.
|
||||
|
||||
If you have a flakes-enabled nix environment, you can try out the tool by running:
|
||||
|
||||
```shell
|
||||
nix run git+https://git.gensokyo.uk/security/fortify -- help
|
||||
```
|
||||
|
||||
## Module usage
|
||||
|
||||
The NixOS module currently requires home-manager to function correctly.
|
||||
|
||||
Full module documentation can be found [here](options.md).
|
||||
The NixOS module currently requires home-manager to configure subordinate users. Full module documentation can be found [here](options.md).
|
||||
|
||||
To use the module, import it into your configuration with
|
||||
|
||||
```nix
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
|
||||
|
||||
fortify = {
|
||||
url = "git+https://git.gensokyo.uk/security/fortify";
|
||||
hakurei = {
|
||||
url = "git+https://git.gensokyo.uk/security/hakurei";
|
||||
|
||||
# Optional but recommended to limit the size of your system closure.
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, fortify, ... }:
|
||||
outputs = { self, nixpkgs, hakurei, ... }:
|
||||
{
|
||||
nixosConfigurations.fortify = nixpkgs.lib.nixosSystem {
|
||||
nixosConfigurations.hakurei = nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
modules = [
|
||||
fortify.nixosModules.fortify
|
||||
hakurei.nixosModules.hakurei
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This adds the `environment.fortify` option:
|
||||
This adds the `environment.hakurei` option:
|
||||
|
||||
```nix
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
environment.fortify = {
|
||||
environment.hakurei = {
|
||||
enable = true;
|
||||
stateDir = "/var/lib/persist/module/fortify";
|
||||
stateDir = "/var/lib/hakurei";
|
||||
users = {
|
||||
alice = 0;
|
||||
nixos = 10;
|
||||
};
|
||||
|
||||
apps = [
|
||||
commonPaths = [
|
||||
{
|
||||
src = "/sdcard";
|
||||
write = true;
|
||||
}
|
||||
];
|
||||
|
||||
extraHomeConfig = {
|
||||
home.stateVersion = "23.05";
|
||||
};
|
||||
|
||||
apps = {
|
||||
"org.chromium.Chromium" = {
|
||||
name = "chromium";
|
||||
id = "org.chromium.Chromium";
|
||||
identity = 1;
|
||||
packages = [ pkgs.chromium ];
|
||||
userns = true;
|
||||
mapRealUid = true;
|
||||
@@ -104,16 +106,20 @@ This adds the `environment.fortify` option:
|
||||
broadcast = { };
|
||||
};
|
||||
};
|
||||
}
|
||||
{
|
||||
};
|
||||
|
||||
"org.claws_mail.Claws-Mail" = {
|
||||
name = "claws-mail";
|
||||
id = "org.claws_mail.Claws-Mail";
|
||||
identity = 2;
|
||||
packages = [ pkgs.claws-mail ];
|
||||
gpu = false;
|
||||
capability.pulse = false;
|
||||
}
|
||||
{
|
||||
};
|
||||
|
||||
"org.weechat" = {
|
||||
name = "weechat";
|
||||
identity = 3;
|
||||
shareUid = true;
|
||||
packages = [ pkgs.weechat ];
|
||||
capability = {
|
||||
wayland = false;
|
||||
@@ -121,10 +127,12 @@ This adds the `environment.fortify` option:
|
||||
dbus = true;
|
||||
pulse = false;
|
||||
};
|
||||
}
|
||||
{
|
||||
};
|
||||
|
||||
"dev.vencord.Vesktop" = {
|
||||
name = "discord";
|
||||
id = "dev.vencord.Vesktop";
|
||||
identity = 3;
|
||||
shareUid = true;
|
||||
packages = [ pkgs.vesktop ];
|
||||
share = pkgs.vesktop;
|
||||
command = "vesktop --ozone-platform-hint=wayland";
|
||||
@@ -142,9 +150,12 @@ This adds the `environment.fortify` option:
|
||||
};
|
||||
system.filter = true;
|
||||
};
|
||||
}
|
||||
{
|
||||
};
|
||||
|
||||
"io.looking-glass" = {
|
||||
name = "looking-glass-client";
|
||||
identity = 4;
|
||||
useCommonPaths = false;
|
||||
groups = [ "plugdev" ];
|
||||
extraPaths = [
|
||||
{
|
||||
@@ -155,8 +166,8 @@ This adds the `environment.fortify` option:
|
||||
extraConfig = {
|
||||
programs.looking-glass-client.enable = true;
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
#include "acl-update.h"
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <sys/acl.h>
|
||||
#include <acl/libacl.h>
|
||||
|
||||
int f_acl_update_file_by_uid(const char *path_p, uid_t uid, acl_perm_t *perms, size_t plen) {
|
||||
int ret = -1;
|
||||
bool v;
|
||||
int i;
|
||||
acl_t acl;
|
||||
acl_entry_t entry;
|
||||
acl_tag_t tag_type;
|
||||
void *qualifier_p;
|
||||
acl_permset_t permset;
|
||||
|
||||
acl = acl_get_file(path_p, ACL_TYPE_ACCESS);
|
||||
if (acl == NULL)
|
||||
goto out;
|
||||
|
||||
// prune entries by uid
|
||||
for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1; i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) {
|
||||
if (acl_get_tag_type(entry, &tag_type) != 0)
|
||||
return -1;
|
||||
if (tag_type != ACL_USER)
|
||||
continue;
|
||||
|
||||
qualifier_p = acl_get_qualifier(entry);
|
||||
if (qualifier_p == NULL)
|
||||
return -1;
|
||||
v = *(uid_t *)qualifier_p == uid;
|
||||
acl_free(qualifier_p);
|
||||
|
||||
if (!v)
|
||||
continue;
|
||||
|
||||
acl_delete_entry(acl, entry);
|
||||
}
|
||||
|
||||
if (plen == 0)
|
||||
goto set;
|
||||
|
||||
if (acl_create_entry(&acl, &entry) != 0)
|
||||
goto out;
|
||||
if (acl_get_permset(entry, &permset) != 0)
|
||||
goto out;
|
||||
for (i = 0; i < plen; i++) {
|
||||
if (acl_add_perm(permset, perms[i]) != 0)
|
||||
goto out;
|
||||
}
|
||||
if (acl_set_tag_type(entry, ACL_USER) != 0)
|
||||
goto out;
|
||||
if (acl_set_qualifier(entry, (void *)&uid) != 0)
|
||||
goto out;
|
||||
|
||||
set:
|
||||
if (acl_calc_mask(&acl) != 0)
|
||||
goto out;
|
||||
if (acl_valid(acl) != 0)
|
||||
goto out;
|
||||
if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0)
|
||||
ret = 0;
|
||||
|
||||
out:
|
||||
free((void *)path_p);
|
||||
if (acl != NULL)
|
||||
acl_free((void *)acl);
|
||||
return ret;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
#include <sys/acl.h>
|
||||
|
||||
int f_acl_update_file_by_uid(const char *path_p, uid_t uid, acl_perm_t *perms, size_t plen);
|
||||
@@ -1,29 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal/app"
|
||||
"git.gensokyo.uk/security/fortify/internal/app/instance"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
)
|
||||
|
||||
func mustRunApp(ctx context.Context, config *fst.Config, beforeFail func()) {
|
||||
rs := new(app.RunState)
|
||||
a := instance.MustNew(instance.ISetuid, ctx, std)
|
||||
|
||||
var code int
|
||||
if sa, err := a.Seal(config); err != nil {
|
||||
fmsg.PrintBaseError(err, "cannot seal app:")
|
||||
code = 1
|
||||
} else {
|
||||
code = instance.PrintRunStateErr(instance.ISetuid, rs, sa.Run(rs))
|
||||
}
|
||||
|
||||
if code != 0 {
|
||||
beforeFail()
|
||||
os.Exit(code)
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
lib,
|
||||
buildGoModule,
|
||||
fortify ? abort "fortify package required",
|
||||
}:
|
||||
|
||||
buildGoModule {
|
||||
pname = "${fortify.pname}-fsu";
|
||||
inherit (fortify) version;
|
||||
|
||||
src = ./.;
|
||||
inherit (fortify) vendorHash;
|
||||
CGO_ENABLED = 0;
|
||||
|
||||
preBuild = ''
|
||||
go mod init fsu >& /dev/null
|
||||
'';
|
||||
|
||||
ldflags =
|
||||
lib.attrsets.foldlAttrs
|
||||
(
|
||||
ldflags: name: value:
|
||||
ldflags ++ [ "-X main.${name}=${value}" ]
|
||||
)
|
||||
[ "-s -w" ]
|
||||
{
|
||||
fmain = "${fortify}/libexec/fortify";
|
||||
fpkg = "${fortify}/libexec/fpkg";
|
||||
};
|
||||
}
|
||||
@@ -2,8 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -15,68 +13,29 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/command"
|
||||
"git.gensokyo.uk/security/fortify/dbus"
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
"git.gensokyo.uk/security/fortify/internal/app"
|
||||
"git.gensokyo.uk/security/fortify/internal/app/instance"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"git.gensokyo.uk/security/fortify/internal/state"
|
||||
"git.gensokyo.uk/security/fortify/internal/sys"
|
||||
"git.gensokyo.uk/security/fortify/sandbox"
|
||||
"git.gensokyo.uk/security/fortify/system"
|
||||
"hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/cmd/hakurei/internal/app/instance"
|
||||
"hakurei.app/cmd/hakurei/internal/state"
|
||||
"hakurei.app/command"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
var (
|
||||
errSuccess = errors.New("success")
|
||||
|
||||
//go:embed LICENSE
|
||||
license string
|
||||
)
|
||||
|
||||
func init() { fmsg.Prepare("fortify") }
|
||||
|
||||
var std sys.State = new(sys.Std)
|
||||
|
||||
func main() {
|
||||
// early init path, skips root check and duplicate PR_SET_DUMPABLE
|
||||
sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
|
||||
|
||||
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
|
||||
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||
// not fatal: this program runs as the privileged user
|
||||
}
|
||||
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("this program must not run as root")
|
||||
}
|
||||
|
||||
buildCommand(os.Stderr).MustParse(os.Args[1:], func(err error) {
|
||||
fmsg.Verbosef("command returned %v", err)
|
||||
if errors.Is(err, errSuccess) {
|
||||
fmsg.BeforeExit()
|
||||
os.Exit(0)
|
||||
}
|
||||
})
|
||||
log.Fatal("unreachable")
|
||||
}
|
||||
|
||||
func buildCommand(out io.Writer) command.Command {
|
||||
var (
|
||||
flagVerbose bool
|
||||
flagJSON bool
|
||||
)
|
||||
c := command.New(out, log.Printf, "fortify", func([]string) error {
|
||||
internal.InstallFmsg(flagVerbose)
|
||||
return nil
|
||||
}).
|
||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
|
||||
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output as JSON when applicable")
|
||||
c := command.New(out, log.Printf, "hakurei", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
|
||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
|
||||
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
|
||||
|
||||
c.Command("shim", command.UsageInternal, func([]string) error { instance.ShimMain(); return errSuccess })
|
||||
|
||||
c.Command("app", "Launch app defined by the specified config file", func(args []string) error {
|
||||
c.Command("app", "Load app from configuration file", func(args []string) error {
|
||||
if len(args) < 1 {
|
||||
log.Fatal("app requires at least 1 argument")
|
||||
}
|
||||
@@ -107,7 +66,7 @@ func buildCommand(out io.Writer) command.Command {
|
||||
|
||||
c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
|
||||
// initialise config from flags
|
||||
config := &fst.Config{
|
||||
config := &hst.Config{
|
||||
ID: fid,
|
||||
Args: args,
|
||||
}
|
||||
@@ -123,19 +82,19 @@ func buildCommand(out io.Writer) command.Command {
|
||||
passwdFunc = func() {
|
||||
var us string
|
||||
if uid, err := std.Uid(aid); err != nil {
|
||||
fmsg.PrintBaseError(err, "cannot obtain uid from fsu:")
|
||||
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
us = strconv.Itoa(uid)
|
||||
}
|
||||
|
||||
if u, err := user.LookupId(us); err != nil {
|
||||
fmsg.Verbosef("cannot look up uid %s", us)
|
||||
hlog.Verbosef("cannot look up uid %s", us)
|
||||
passwd = &user.User{
|
||||
Uid: us,
|
||||
Gid: us,
|
||||
Username: "chronos",
|
||||
Name: "Fortify",
|
||||
Name: "Hakurei Permissive Default",
|
||||
HomeDir: "/var/empty",
|
||||
}
|
||||
} else {
|
||||
@@ -205,35 +164,35 @@ func buildCommand(out io.Writer) command.Command {
|
||||
panic("unreachable")
|
||||
}).
|
||||
Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
|
||||
"Path to D-Bus proxy config file, or \"builtin\" for defaults").
|
||||
"Path to session bus proxy config file, or \"builtin\" for defaults").
|
||||
Flag(&dbusConfigSystem, "dbus-system", command.StringFlag("nil"),
|
||||
"Path to system D-Bus proxy config file, or \"nil\" to disable").
|
||||
"Path to system bus proxy config file, or \"nil\" to disable").
|
||||
Flag(&mpris, "mpris", command.BoolFlag(false),
|
||||
"Allow owning MPRIS D-Bus path, has no effect if custom config is available").
|
||||
Flag(&dbusVerbose, "dbus-log", command.BoolFlag(false),
|
||||
"Force logging in the D-Bus proxy").
|
||||
"Force buffered logging in the D-Bus proxy").
|
||||
Flag(&fid, "id", command.StringFlag(""),
|
||||
"App ID, leave empty to disable security context app_id").
|
||||
"Reverse-DNS style Application identifier, leave empty to inherit instance identifier").
|
||||
Flag(&aid, "a", command.IntFlag(0),
|
||||
"Fortify application ID").
|
||||
"Application identity").
|
||||
Flag(nil, "g", &groups,
|
||||
"Groups inherited by the app process").
|
||||
"Groups inherited by all container processes").
|
||||
Flag(&homeDir, "d", command.StringFlag("os"),
|
||||
"Application home directory").
|
||||
"Container home directory").
|
||||
Flag(&userName, "u", command.StringFlag("chronos"),
|
||||
"Passwd name within sandbox").
|
||||
"Passwd user name within sandbox").
|
||||
Flag(&wayland, "wayland", command.BoolFlag(false),
|
||||
"Allow Wayland connections").
|
||||
"Enable connection to Wayland via security-context-v1").
|
||||
Flag(&x11, "X", command.BoolFlag(false),
|
||||
"Share X11 socket and allow connection").
|
||||
"Enable direct connection to X11").
|
||||
Flag(&dBus, "dbus", command.BoolFlag(false),
|
||||
"Proxy D-Bus connection").
|
||||
"Enable proxied connection to D-Bus").
|
||||
Flag(&pulse, "pulse", command.BoolFlag(false),
|
||||
"Share PulseAudio socket and cookie")
|
||||
"Enable direct connection to PulseAudio")
|
||||
}
|
||||
|
||||
var showFlagShort bool
|
||||
c.NewCommand("show", "Show the contents of an app configuration", func(args []string) error {
|
||||
c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
|
||||
switch len(args) {
|
||||
case 0: // system
|
||||
printShowSystem(os.Stdout, showFlagShort, flagJSON)
|
||||
@@ -253,12 +212,12 @@ func buildCommand(out io.Writer) command.Command {
|
||||
}).Flag(&showFlagShort, "short", command.BoolFlag(false), "Omit filesystem information")
|
||||
|
||||
var psFlagShort bool
|
||||
c.NewCommand("ps", "List active apps and their state", func(args []string) error {
|
||||
c.NewCommand("ps", "List active instances", func(args []string) error {
|
||||
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath), psFlagShort, flagJSON)
|
||||
return errSuccess
|
||||
}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")
|
||||
|
||||
c.Command("version", "Show fortify version", func(args []string) error {
|
||||
c.Command("version", "Display version information", func(args []string) error {
|
||||
fmt.Println(internal.Version())
|
||||
return errSuccess
|
||||
})
|
||||
@@ -269,7 +228,7 @@ func buildCommand(out io.Writer) command.Command {
|
||||
})
|
||||
|
||||
c.Command("template", "Produce a config template", func(args []string) error {
|
||||
printJSON(os.Stdout, false, fst.Template())
|
||||
printJSON(os.Stdout, false, hst.Template())
|
||||
return errSuccess
|
||||
})
|
||||
|
||||
@@ -281,7 +240,7 @@ func buildCommand(out io.Writer) command.Command {
|
||||
return c
|
||||
}
|
||||
|
||||
func runApp(config *fst.Config) {
|
||||
func runApp(config *hst.Config) {
|
||||
ctx, stop := signal.NotifyContext(context.Background(),
|
||||
syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop() // unreachable
|
||||
@@ -289,7 +248,7 @@ func runApp(config *fst.Config) {
|
||||
|
||||
rs := new(app.RunState)
|
||||
if sa, err := a.Seal(config); err != nil {
|
||||
fmsg.PrintBaseError(err, "cannot seal app:")
|
||||
hlog.PrintBaseError(err, "cannot seal app:")
|
||||
internal.Exit(1)
|
||||
} else {
|
||||
internal.Exit(instance.PrintRunStateErr(instance.ISetuid, rs, sa.Run(rs)))
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/command"
|
||||
"hakurei.app/command"
|
||||
)
|
||||
|
||||
func TestHelp(t *testing.T) {
|
||||
@@ -17,14 +17,14 @@ func TestHelp(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
"main", []string{}, `
|
||||
Usage: fortify [-h | --help] [-v] [--json] COMMAND [OPTIONS]
|
||||
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
app Launch app defined by the specified config file
|
||||
app Load app from configuration 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
|
||||
show Show live or local app configuration
|
||||
ps List active instances
|
||||
version Display version information
|
||||
license Show full license text
|
||||
template Produce a config template
|
||||
help Show this help message
|
||||
@@ -33,34 +33,34 @@ Commands:
|
||||
},
|
||||
{
|
||||
"run", []string{"run", "-h"}, `
|
||||
Usage: fortify run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--wayland] [-X] [--dbus] [--pulse] COMMAND [OPTIONS]
|
||||
Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--wayland] [-X] [--dbus] [--pulse] COMMAND [OPTIONS]
|
||||
|
||||
Flags:
|
||||
-X Share X11 socket and allow connection
|
||||
-X Enable direct connection to X11
|
||||
-a int
|
||||
Fortify application ID
|
||||
Application identity
|
||||
-d string
|
||||
Application home directory (default "os")
|
||||
Container home directory (default "os")
|
||||
-dbus
|
||||
Proxy D-Bus connection
|
||||
Enable proxied connection to D-Bus
|
||||
-dbus-config string
|
||||
Path to D-Bus proxy config file, or "builtin" for defaults (default "builtin")
|
||||
Path to session bus proxy config file, or "builtin" for defaults (default "builtin")
|
||||
-dbus-log
|
||||
Force logging in the D-Bus proxy
|
||||
Force buffered logging in the D-Bus proxy
|
||||
-dbus-system string
|
||||
Path to system D-Bus proxy config file, or "nil" to disable (default "nil")
|
||||
Path to system bus proxy config file, or "nil" to disable (default "nil")
|
||||
-g value
|
||||
Groups inherited by the app process
|
||||
Groups inherited by all container processes
|
||||
-id string
|
||||
App ID, leave empty to disable security context app_id
|
||||
Reverse-DNS style Application identifier, leave empty to inherit instance identifier
|
||||
-mpris
|
||||
Allow owning MPRIS D-Bus path, has no effect if custom config is available
|
||||
-pulse
|
||||
Share PulseAudio socket and cookie
|
||||
Enable direct connection to PulseAudio
|
||||
-u string
|
||||
Passwd name within sandbox (default "chronos")
|
||||
Passwd user name within sandbox (default "chronos")
|
||||
-wayland
|
||||
Allow Wayland connections
|
||||
Enable connection to Wayland via security-context-v1
|
||||
|
||||
`,
|
||||
},
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"hakurei.app/hst"
|
||||
)
|
||||
|
||||
type App interface {
|
||||
@@ -14,7 +14,7 @@ type App interface {
|
||||
|
||||
// Seal determines the outcome of config as a [SealedApp].
|
||||
// The value of config might be overwritten and must not be used again.
|
||||
Seal(config *fst.Config) (SealedApp, error)
|
||||
Seal(config *hst.Config) (SealedApp, error)
|
||||
|
||||
String() string
|
||||
}
|
||||
@@ -47,13 +47,3 @@ func (rs *RunState) SetStart() {
|
||||
now := time.Now().UTC()
|
||||
rs.Time = &now
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
. "git.gensokyo.uk/security/fortify/internal/app"
|
||||
. "hakurei.app/cmd/hakurei/internal/app"
|
||||
)
|
||||
|
||||
func TestParseAppID(t *testing.T) {
|
||||
@@ -8,71 +8,74 @@ import (
|
||||
"path"
|
||||
"syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/dbus"
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal/sys"
|
||||
"git.gensokyo.uk/security/fortify/sandbox"
|
||||
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/sys"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
// in practice there should be less than 30 entries added by the runtime;
|
||||
// allocating slightly more as a margin for future expansion
|
||||
const preallocateOpsCount = 1 << 5
|
||||
|
||||
// NewContainer initialises [sandbox.Params] via [fst.ContainerConfig].
|
||||
// NewContainer initialises [sandbox.Params] via [hst.ContainerConfig].
|
||||
// Note that remaining container setup must be queued by the caller.
|
||||
func NewContainer(s *fst.ContainerConfig, os sys.State, uid, gid *int) (*sandbox.Params, map[string]string, error) {
|
||||
func NewContainer(s *hst.ContainerConfig, os sys.State, uid, gid *int) (*container.Params, map[string]string, error) {
|
||||
if s == nil {
|
||||
return nil, nil, syscall.EBADE
|
||||
}
|
||||
|
||||
container := &sandbox.Params{
|
||||
Hostname: s.Hostname,
|
||||
Seccomp: s.Seccomp,
|
||||
params := &container.Params{
|
||||
Hostname: s.Hostname,
|
||||
SeccompFlags: s.SeccompFlags,
|
||||
SeccompPresets: s.SeccompPresets,
|
||||
RetainSession: s.Tty,
|
||||
HostNet: s.Net,
|
||||
}
|
||||
|
||||
{
|
||||
ops := make(sandbox.Ops, 0, preallocateOpsCount+len(s.Filesystem)+len(s.Link)+len(s.Cover))
|
||||
container.Ops = &ops
|
||||
ops := make(container.Ops, 0, preallocateOpsCount+len(s.Filesystem)+len(s.Link)+len(s.Cover))
|
||||
params.Ops = &ops
|
||||
}
|
||||
|
||||
if s.Multiarch {
|
||||
container.Seccomp |= seccomp.FilterMultiarch
|
||||
params.SeccompFlags |= seccomp.AllowMultiarch
|
||||
}
|
||||
|
||||
if s.Devel {
|
||||
container.Flags |= sandbox.FAllowDevel
|
||||
if !s.SeccompCompat {
|
||||
params.SeccompPresets |= seccomp.PresetExt
|
||||
}
|
||||
if s.Userns {
|
||||
container.Flags |= sandbox.FAllowUserns
|
||||
if !s.Devel {
|
||||
params.SeccompPresets |= seccomp.PresetDenyDevel
|
||||
}
|
||||
if s.Net {
|
||||
container.Flags |= sandbox.FAllowNet
|
||||
if !s.Userns {
|
||||
params.SeccompPresets |= seccomp.PresetDenyNS
|
||||
}
|
||||
if s.Tty {
|
||||
container.Flags |= sandbox.FAllowTTY
|
||||
if !s.Tty {
|
||||
params.SeccompPresets |= seccomp.PresetDenyTTY
|
||||
}
|
||||
|
||||
if s.MapRealUID {
|
||||
/* some programs fail to connect to dbus session running as a different uid
|
||||
so this workaround is introduced to map priv-side caller uid in container */
|
||||
container.Uid = os.Getuid()
|
||||
*uid = container.Uid
|
||||
container.Gid = os.Getgid()
|
||||
*gid = container.Gid
|
||||
params.Uid = os.Getuid()
|
||||
*uid = params.Uid
|
||||
params.Gid = os.Getgid()
|
||||
*gid = params.Gid
|
||||
} else {
|
||||
*uid = sandbox.OverflowUid()
|
||||
*gid = sandbox.OverflowGid()
|
||||
*uid = container.OverflowUid()
|
||||
*gid = container.OverflowGid()
|
||||
}
|
||||
|
||||
container.
|
||||
params.
|
||||
Proc("/proc").
|
||||
Tmpfs(fst.Tmp, 1<<12, 0755)
|
||||
Tmpfs(hst.Tmp, 1<<12, 0755)
|
||||
|
||||
if !s.Device {
|
||||
container.Dev("/dev").Mqueue("/dev/mqueue")
|
||||
params.Dev("/dev").Mqueue("/dev/mqueue")
|
||||
} else {
|
||||
container.Bind("/dev", "/dev", sandbox.BindWritable|sandbox.BindDevice)
|
||||
params.Bind("/dev", "/dev", container.BindWritable|container.BindDevice)
|
||||
}
|
||||
|
||||
/* retrieve paths and hide them if they're made available in the sandbox;
|
||||
@@ -151,29 +154,29 @@ func NewContainer(s *fst.ContainerConfig, os sys.State, uid, gid *int) (*sandbox
|
||||
|
||||
var flags int
|
||||
if c.Write {
|
||||
flags |= sandbox.BindWritable
|
||||
flags |= container.BindWritable
|
||||
}
|
||||
if c.Device {
|
||||
flags |= sandbox.BindDevice | sandbox.BindWritable
|
||||
flags |= container.BindDevice | container.BindWritable
|
||||
}
|
||||
if !c.Must {
|
||||
flags |= sandbox.BindOptional
|
||||
flags |= container.BindOptional
|
||||
}
|
||||
container.Bind(c.Src, dest, flags)
|
||||
params.Bind(c.Src, dest, flags)
|
||||
}
|
||||
|
||||
// cover matched paths
|
||||
for i, ok := range hidePathMatch {
|
||||
if ok {
|
||||
container.Tmpfs(hidePaths[i], 1<<13, 0755)
|
||||
params.Tmpfs(hidePaths[i], 1<<13, 0755)
|
||||
}
|
||||
}
|
||||
|
||||
for _, l := range s.Link {
|
||||
container.Link(l[0], l[1])
|
||||
params.Link(l[0], l[1])
|
||||
}
|
||||
|
||||
return container, maps.Clone(s.Env), nil
|
||||
return params, maps.Clone(s.Env), nil
|
||||
}
|
||||
|
||||
func evalSymlinks(os sys.State, v *string) error {
|
||||
@@ -3,8 +3,8 @@ package instance
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/internal/app"
|
||||
"git.gensokyo.uk/security/fortify/internal/app/internal/setuid"
|
||||
"hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/cmd/hakurei/internal/app/internal/setuid"
|
||||
)
|
||||
|
||||
func PrintRunStateErr(whence int, rs *app.RunState, runErr error) (code int) {
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"log"
|
||||
"syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/internal/app"
|
||||
"git.gensokyo.uk/security/fortify/internal/app/internal/setuid"
|
||||
"git.gensokyo.uk/security/fortify/internal/sys"
|
||||
"hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/cmd/hakurei/internal/app/internal/setuid"
|
||||
"hakurei.app/internal/sys"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -1,6 +1,6 @@
|
||||
package instance
|
||||
|
||||
import "git.gensokyo.uk/security/fortify/internal/app/internal/setuid"
|
||||
import "hakurei.app/cmd/hakurei/internal/app/internal/setuid"
|
||||
|
||||
// ShimMain is the main function of the shim process and runs as the unconstrained target user.
|
||||
func ShimMain() { setuid.ShimMain() }
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
. "git.gensokyo.uk/security/fortify/internal/app"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"git.gensokyo.uk/security/fortify/internal/sys"
|
||||
. "hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/internal/sys"
|
||||
)
|
||||
|
||||
func New(ctx context.Context, os sys.State) (App, error) {
|
||||
@@ -52,7 +52,7 @@ func (a *app) String() string {
|
||||
return fmt.Sprintf("(unsealed app %s)", a.id)
|
||||
}
|
||||
|
||||
func (a *app) Seal(config *fst.Config) (SealedApp, error) {
|
||||
func (a *app) Seal(config *hst.Config) (SealedApp, error) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
@@ -60,7 +60,7 @@ func (a *app) Seal(config *fst.Config) (SealedApp, error) {
|
||||
panic("app sealed twice")
|
||||
}
|
||||
if config == nil {
|
||||
return nil, fmsg.WrapError(ErrConfig,
|
||||
return nil, hlog.WrapErr(ErrConfig,
|
||||
"attempted to seal app with nil config")
|
||||
}
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
package setuid_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/internal/app"
|
||||
"git.gensokyo.uk/security/fortify/sandbox"
|
||||
"git.gensokyo.uk/security/fortify/system"
|
||||
"hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
var testCasesNixos = []sealTestCase{
|
||||
{
|
||||
"nixos chromium direct wayland", new(stubNixOS),
|
||||
&fst.Config{
|
||||
&hst.Config{
|
||||
ID: "org.chromium.Chromium",
|
||||
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
|
||||
Enablements: system.EWayland | system.EDBus | system.EPulse,
|
||||
|
||||
Container: &fst.ContainerConfig{
|
||||
Container: &hst.ContainerConfig{
|
||||
Userns: true, Net: true, MapRealUID: true, Env: nil, AutoEtc: true,
|
||||
Filesystem: []*fst.FilesystemConfig{
|
||||
Filesystem: []*hst.FilesystemConfig{
|
||||
{Src: "/bin", Must: true}, {Src: "/usr/bin", Must: true},
|
||||
{Src: "/nix/store", Must: true}, {Src: "/run/current-system", Must: true},
|
||||
{Src: "/sys/block"}, {Src: "/sys/bus"}, {Src: "/sys/class"}, {Src: "/sys/dev"}, {Src: "/sys/devices"},
|
||||
@@ -48,7 +49,7 @@ var testCasesNixos = []sealTestCase{
|
||||
DirectWayland: true,
|
||||
|
||||
Username: "u0_a1",
|
||||
Data: "/var/lib/persist/module/fortify/0/1",
|
||||
Data: "/var/lib/persist/module/hakurei/0/1",
|
||||
Identity: 1, Groups: []string{},
|
||||
},
|
||||
app.ID{
|
||||
@@ -58,17 +59,19 @@ var testCasesNixos = []sealTestCase{
|
||||
0xb4, 0x6e, 0xb5, 0xc1,
|
||||
},
|
||||
system.New(1000001).
|
||||
Ensure("/tmp/fortify.1971", 0711).
|
||||
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).
|
||||
Ensure("/run/user/1971/fortify", 0700).UpdatePermType(system.User, "/run/user/1971/fortify", acl.Execute).
|
||||
Ensure("/tmp/hakurei.1971", 0711).
|
||||
Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute).
|
||||
Ensure("/tmp/hakurei.1971/runtime/1", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/1", acl.Read, acl.Write, acl.Execute).
|
||||
Ensure("/tmp/hakurei.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir", acl.Execute).
|
||||
Ensure("/tmp/hakurei.1971/tmpdir/1", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/1", acl.Read, acl.Write, acl.Execute).
|
||||
Ensure("/run/user/1971/hakurei", 0700).UpdatePermType(system.User, "/run/user/1971/hakurei", acl.Execute).
|
||||
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
|
||||
UpdatePermType(system.EWayland, "/run/user/1971/wayland-0", acl.Read, acl.Write, acl.Execute).
|
||||
Ephemeral(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute).
|
||||
Link("/run/user/1971/pulse/native", "/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse").
|
||||
Ephemeral(system.Process, "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1", 0700).UpdatePermType(system.Process, "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1", acl.Execute).
|
||||
Link("/run/user/1971/pulse/native", "/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse").
|
||||
CopyFile(nil, "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
|
||||
Ephemeral(system.Process, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1", 0711).
|
||||
MustProxyDBus("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", &dbus.Config{
|
||||
Ephemeral(system.Process, "/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1", 0711).
|
||||
MustProxyDBus("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", &dbus.Config{
|
||||
Talk: []string{
|
||||
"org.freedesktop.FileManager1", "org.freedesktop.Notifications",
|
||||
"org.freedesktop.ScreenSaver", "org.freedesktop.secrets",
|
||||
@@ -81,7 +84,7 @@ var testCasesNixos = []sealTestCase{
|
||||
},
|
||||
Call: map[string]string{}, Broadcast: map[string]string{},
|
||||
Filter: true,
|
||||
}, "/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", &dbus.Config{
|
||||
}, "/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", &dbus.Config{
|
||||
Talk: []string{
|
||||
"org.bluez",
|
||||
"org.freedesktop.Avahi",
|
||||
@@ -89,20 +92,19 @@ var testCasesNixos = []sealTestCase{
|
||||
},
|
||||
Filter: true,
|
||||
}).
|
||||
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
|
||||
UpdatePerm("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
|
||||
&sandbox.Params{
|
||||
Uid: 1971,
|
||||
Gid: 100,
|
||||
Flags: sandbox.FAllowNet | sandbox.FAllowUserns,
|
||||
Dir: "/var/lib/persist/module/fortify/0/1",
|
||||
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
|
||||
Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
|
||||
UpdatePerm("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", acl.Read, acl.Write).
|
||||
UpdatePerm("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", acl.Read, acl.Write),
|
||||
&container.Params{
|
||||
Uid: 1971,
|
||||
Gid: 100,
|
||||
Dir: "/var/lib/persist/module/hakurei/0/1",
|
||||
Path: "/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start",
|
||||
Args: []string{"/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"},
|
||||
Env: []string{
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1971/bus",
|
||||
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
|
||||
"HOME=/var/lib/persist/module/fortify/0/1",
|
||||
"PULSE_COOKIE=" + fst.Tmp + "/pulse-cookie",
|
||||
"HOME=/var/lib/persist/module/hakurei/0/1",
|
||||
"PULSE_COOKIE=" + hst.Tmp + "/pulse-cookie",
|
||||
"PULSE_SERVER=unix:/run/user/1971/pulse/native",
|
||||
"SHELL=/run/current-system/sw/bin/zsh",
|
||||
"TERM=xterm-256color",
|
||||
@@ -112,34 +114,36 @@ var testCasesNixos = []sealTestCase{
|
||||
"XDG_SESSION_CLASS=user",
|
||||
"XDG_SESSION_TYPE=tty",
|
||||
},
|
||||
Ops: new(sandbox.Ops).
|
||||
Ops: new(container.Ops).
|
||||
Proc("/proc").
|
||||
Tmpfs(fst.Tmp, 4096, 0755).
|
||||
Tmpfs(hst.Tmp, 4096, 0755).
|
||||
Dev("/dev").Mqueue("/dev/mqueue").
|
||||
Bind("/bin", "/bin", 0).
|
||||
Bind("/usr/bin", "/usr/bin", 0).
|
||||
Bind("/nix/store", "/nix/store", 0).
|
||||
Bind("/run/current-system", "/run/current-system", 0).
|
||||
Bind("/sys/block", "/sys/block", sandbox.BindOptional).
|
||||
Bind("/sys/bus", "/sys/bus", sandbox.BindOptional).
|
||||
Bind("/sys/class", "/sys/class", sandbox.BindOptional).
|
||||
Bind("/sys/dev", "/sys/dev", sandbox.BindOptional).
|
||||
Bind("/sys/devices", "/sys/devices", sandbox.BindOptional).
|
||||
Bind("/sys/block", "/sys/block", container.BindOptional).
|
||||
Bind("/sys/bus", "/sys/bus", container.BindOptional).
|
||||
Bind("/sys/class", "/sys/class", container.BindOptional).
|
||||
Bind("/sys/dev", "/sys/dev", container.BindOptional).
|
||||
Bind("/sys/devices", "/sys/devices", container.BindOptional).
|
||||
Bind("/run/opengl-driver", "/run/opengl-driver", 0).
|
||||
Bind("/dev/dri", "/dev/dri", sandbox.BindDevice|sandbox.BindWritable|sandbox.BindOptional).
|
||||
Bind("/dev/dri", "/dev/dri", container.BindDevice|container.BindWritable|container.BindOptional).
|
||||
Etc("/etc", "8e2c76b066dabe574cf073bdb46eb5c1").
|
||||
Tmpfs("/run/user", 4096, 0755).
|
||||
Tmpfs("/run/user/1971", 8388608, 0700).
|
||||
Bind("/tmp/fortify.1971/tmpdir/1", "/tmp", sandbox.BindWritable).
|
||||
Bind("/var/lib/persist/module/fortify/0/1", "/var/lib/persist/module/fortify/0/1", sandbox.BindWritable).
|
||||
Place("/etc/passwd", []byte("u0_a1:x:1971:100:Fortify:/var/lib/persist/module/fortify/0/1:/run/current-system/sw/bin/zsh\n")).
|
||||
Place("/etc/group", []byte("fortify:x:100:\n")).
|
||||
Bind("/tmp/hakurei.1971/runtime/1", "/run/user/1971", container.BindWritable).
|
||||
Bind("/tmp/hakurei.1971/tmpdir/1", "/tmp", container.BindWritable).
|
||||
Bind("/var/lib/persist/module/hakurei/0/1", "/var/lib/persist/module/hakurei/0/1", container.BindWritable).
|
||||
Place("/etc/passwd", []byte("u0_a1:x:1971:100:Hakurei:/var/lib/persist/module/hakurei/0/1:/run/current-system/sw/bin/zsh\n")).
|
||||
Place("/etc/group", []byte("hakurei:x:100:\n")).
|
||||
Bind("/run/user/1971/wayland-0", "/run/user/1971/wayland-0", 0).
|
||||
Bind("/run/user/1971/fortify/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native", 0).
|
||||
Place(fst.Tmp+"/pulse-cookie", nil).
|
||||
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus", 0).
|
||||
Bind("/tmp/fortify.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket", 0).
|
||||
Bind("/run/user/1971/hakurei/8e2c76b066dabe574cf073bdb46eb5c1/pulse", "/run/user/1971/pulse/native", 0).
|
||||
Place(hst.Tmp+"/pulse-cookie", nil).
|
||||
Bind("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/bus", "/run/user/1971/bus", 0).
|
||||
Bind("/tmp/hakurei.1971/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", "/run/dbus/system_bus_socket", 0).
|
||||
Tmpfs("/var/run/nscd", 8192, 0755),
|
||||
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyTTY | seccomp.PresetDenyDevel,
|
||||
HostNet: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
225
cmd/hakurei/internal/app/internal/setuid/app_pd_test.go
Normal file
225
cmd/hakurei/internal/app/internal/setuid/app_pd_test.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package setuid_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
var testCasesPd = []sealTestCase{
|
||||
{
|
||||
"nixos permissive defaults no enablements", new(stubNixOS),
|
||||
&hst.Config{Username: "chronos", Data: "/home/chronos"},
|
||||
app.ID{
|
||||
0x4a, 0x45, 0x0b, 0x65,
|
||||
0x96, 0xd7, 0xbc, 0x15,
|
||||
0xbd, 0x01, 0x78, 0x0e,
|
||||
0xb9, 0xa6, 0x07, 0xac,
|
||||
},
|
||||
system.New(1000000).
|
||||
Ensure("/tmp/hakurei.1971", 0711).
|
||||
Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute).
|
||||
Ensure("/tmp/hakurei.1971/runtime/0", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/0", acl.Read, acl.Write, acl.Execute).
|
||||
Ensure("/tmp/hakurei.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir", acl.Execute).
|
||||
Ensure("/tmp/hakurei.1971/tmpdir/0", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/0", acl.Read, acl.Write, acl.Execute),
|
||||
&container.Params{
|
||||
Dir: "/home/chronos",
|
||||
Path: "/run/current-system/sw/bin/zsh",
|
||||
Args: []string{"/run/current-system/sw/bin/zsh"},
|
||||
Env: []string{
|
||||
"HOME=/home/chronos",
|
||||
"SHELL=/run/current-system/sw/bin/zsh",
|
||||
"TERM=xterm-256color",
|
||||
"USER=chronos",
|
||||
"XDG_RUNTIME_DIR=/run/user/65534",
|
||||
"XDG_SESSION_CLASS=user",
|
||||
"XDG_SESSION_TYPE=tty",
|
||||
},
|
||||
Ops: new(container.Ops).
|
||||
Proc("/proc").
|
||||
Tmpfs(hst.Tmp, 4096, 0755).
|
||||
Dev("/dev").Mqueue("/dev/mqueue").
|
||||
Bind("/bin", "/bin", container.BindWritable).
|
||||
Bind("/boot", "/boot", container.BindWritable).
|
||||
Bind("/home", "/home", container.BindWritable).
|
||||
Bind("/lib", "/lib", container.BindWritable).
|
||||
Bind("/lib64", "/lib64", container.BindWritable).
|
||||
Bind("/nix", "/nix", container.BindWritable).
|
||||
Bind("/root", "/root", container.BindWritable).
|
||||
Bind("/run", "/run", container.BindWritable).
|
||||
Bind("/srv", "/srv", container.BindWritable).
|
||||
Bind("/sys", "/sys", container.BindWritable).
|
||||
Bind("/usr", "/usr", container.BindWritable).
|
||||
Bind("/var", "/var", container.BindWritable).
|
||||
Bind("/dev/kvm", "/dev/kvm", container.BindWritable|container.BindDevice|container.BindOptional).
|
||||
Tmpfs("/run/user/1971", 8192, 0755).
|
||||
Tmpfs("/run/dbus", 8192, 0755).
|
||||
Etc("/etc", "4a450b6596d7bc15bd01780eb9a607ac").
|
||||
Tmpfs("/run/user", 4096, 0755).
|
||||
Bind("/tmp/hakurei.1971/runtime/0", "/run/user/65534", container.BindWritable).
|
||||
Bind("/tmp/hakurei.1971/tmpdir/0", "/tmp", container.BindWritable).
|
||||
Bind("/home/chronos", "/home/chronos", container.BindWritable).
|
||||
Place("/etc/passwd", []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
|
||||
Place("/etc/group", []byte("hakurei:x:65534:\n")).
|
||||
Tmpfs("/var/run/nscd", 8192, 0755),
|
||||
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
|
||||
HostNet: true,
|
||||
RetainSession: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"nixos permissive defaults chromium", new(stubNixOS),
|
||||
&hst.Config{
|
||||
ID: "org.chromium.Chromium",
|
||||
Args: []string{"zsh", "-c", "exec chromium "},
|
||||
Identity: 9,
|
||||
Groups: []string{"video"},
|
||||
Username: "chronos",
|
||||
Data: "/home/chronos",
|
||||
SessionBus: &dbus.Config{
|
||||
Talk: []string{
|
||||
"org.freedesktop.Notifications",
|
||||
"org.freedesktop.FileManager1",
|
||||
"org.freedesktop.ScreenSaver",
|
||||
"org.freedesktop.secrets",
|
||||
"org.kde.kwalletd5",
|
||||
"org.kde.kwalletd6",
|
||||
"org.gnome.SessionManager",
|
||||
},
|
||||
Own: []string{
|
||||
"org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.chromium.*",
|
||||
},
|
||||
Call: map[string]string{
|
||||
"org.freedesktop.portal.*": "*",
|
||||
},
|
||||
Broadcast: map[string]string{
|
||||
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
|
||||
},
|
||||
Filter: true,
|
||||
},
|
||||
SystemBus: &dbus.Config{
|
||||
Talk: []string{
|
||||
"org.bluez",
|
||||
"org.freedesktop.Avahi",
|
||||
"org.freedesktop.UPower",
|
||||
},
|
||||
Filter: true,
|
||||
},
|
||||
Enablements: system.EWayland | system.EDBus | system.EPulse,
|
||||
},
|
||||
app.ID{
|
||||
0xeb, 0xf0, 0x83, 0xd1,
|
||||
0xb1, 0x75, 0x91, 0x17,
|
||||
0x82, 0xd4, 0x13, 0x36,
|
||||
0x9b, 0x64, 0xce, 0x7c,
|
||||
},
|
||||
system.New(1000009).
|
||||
Ensure("/tmp/hakurei.1971", 0711).
|
||||
Ensure("/tmp/hakurei.1971/runtime", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime", acl.Execute).
|
||||
Ensure("/tmp/hakurei.1971/runtime/9", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/runtime/9", acl.Read, acl.Write, acl.Execute).
|
||||
Ensure("/tmp/hakurei.1971/tmpdir", 0700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir", acl.Execute).
|
||||
Ensure("/tmp/hakurei.1971/tmpdir/9", 01700).UpdatePermType(system.User, "/tmp/hakurei.1971/tmpdir/9", acl.Read, acl.Write, acl.Execute).
|
||||
Ephemeral(system.Process, "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c", 0711).
|
||||
Wayland(new(*os.File), "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/1971/wayland-0", "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c").
|
||||
Ensure("/run/user/1971/hakurei", 0700).UpdatePermType(system.User, "/run/user/1971/hakurei", acl.Execute).
|
||||
Ensure("/run/user/1971", 0700).UpdatePermType(system.User, "/run/user/1971", acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset
|
||||
Ephemeral(system.Process, "/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c", 0700).UpdatePermType(system.Process, "/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c", acl.Execute).
|
||||
Link("/run/user/1971/pulse/native", "/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse").
|
||||
CopyFile(new([]byte), "/home/ophestra/xdg/config/pulse/cookie", 256, 256).
|
||||
MustProxyDBus("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", &dbus.Config{
|
||||
Talk: []string{
|
||||
"org.freedesktop.Notifications",
|
||||
"org.freedesktop.FileManager1",
|
||||
"org.freedesktop.ScreenSaver",
|
||||
"org.freedesktop.secrets",
|
||||
"org.kde.kwalletd5",
|
||||
"org.kde.kwalletd6",
|
||||
"org.gnome.SessionManager",
|
||||
},
|
||||
Own: []string{
|
||||
"org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.org.chromium.Chromium.*",
|
||||
"org.mpris.MediaPlayer2.chromium.*",
|
||||
},
|
||||
Call: map[string]string{
|
||||
"org.freedesktop.portal.*": "*",
|
||||
},
|
||||
Broadcast: map[string]string{
|
||||
"org.freedesktop.portal.*": "@/org/freedesktop/portal/*",
|
||||
},
|
||||
Filter: true,
|
||||
}, "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", &dbus.Config{
|
||||
Talk: []string{
|
||||
"org.bluez",
|
||||
"org.freedesktop.Avahi",
|
||||
"org.freedesktop.UPower",
|
||||
},
|
||||
Filter: true,
|
||||
}).
|
||||
UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", acl.Read, acl.Write).
|
||||
UpdatePerm("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", acl.Read, acl.Write),
|
||||
&container.Params{
|
||||
Dir: "/home/chronos",
|
||||
Path: "/run/current-system/sw/bin/zsh",
|
||||
Args: []string{"zsh", "-c", "exec chromium "},
|
||||
Env: []string{
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus",
|
||||
"DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket",
|
||||
"HOME=/home/chronos",
|
||||
"PULSE_COOKIE=" + hst.Tmp + "/pulse-cookie",
|
||||
"PULSE_SERVER=unix:/run/user/65534/pulse/native",
|
||||
"SHELL=/run/current-system/sw/bin/zsh",
|
||||
"TERM=xterm-256color",
|
||||
"USER=chronos",
|
||||
"WAYLAND_DISPLAY=wayland-0",
|
||||
"XDG_RUNTIME_DIR=/run/user/65534",
|
||||
"XDG_SESSION_CLASS=user",
|
||||
"XDG_SESSION_TYPE=tty",
|
||||
},
|
||||
Ops: new(container.Ops).
|
||||
Proc("/proc").
|
||||
Tmpfs(hst.Tmp, 4096, 0755).
|
||||
Dev("/dev").Mqueue("/dev/mqueue").
|
||||
Bind("/bin", "/bin", container.BindWritable).
|
||||
Bind("/boot", "/boot", container.BindWritable).
|
||||
Bind("/home", "/home", container.BindWritable).
|
||||
Bind("/lib", "/lib", container.BindWritable).
|
||||
Bind("/lib64", "/lib64", container.BindWritable).
|
||||
Bind("/nix", "/nix", container.BindWritable).
|
||||
Bind("/root", "/root", container.BindWritable).
|
||||
Bind("/run", "/run", container.BindWritable).
|
||||
Bind("/srv", "/srv", container.BindWritable).
|
||||
Bind("/sys", "/sys", container.BindWritable).
|
||||
Bind("/usr", "/usr", container.BindWritable).
|
||||
Bind("/var", "/var", container.BindWritable).
|
||||
Bind("/dev/dri", "/dev/dri", container.BindWritable|container.BindDevice|container.BindOptional).
|
||||
Bind("/dev/kvm", "/dev/kvm", container.BindWritable|container.BindDevice|container.BindOptional).
|
||||
Tmpfs("/run/user/1971", 8192, 0755).
|
||||
Tmpfs("/run/dbus", 8192, 0755).
|
||||
Etc("/etc", "ebf083d1b175911782d413369b64ce7c").
|
||||
Tmpfs("/run/user", 4096, 0755).
|
||||
Bind("/tmp/hakurei.1971/runtime/9", "/run/user/65534", container.BindWritable).
|
||||
Bind("/tmp/hakurei.1971/tmpdir/9", "/tmp", container.BindWritable).
|
||||
Bind("/home/chronos", "/home/chronos", container.BindWritable).
|
||||
Place("/etc/passwd", []byte("chronos:x:65534:65534:Hakurei:/home/chronos:/run/current-system/sw/bin/zsh\n")).
|
||||
Place("/etc/group", []byte("hakurei:x:65534:\n")).
|
||||
Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", "/run/user/65534/wayland-0", 0).
|
||||
Bind("/run/user/1971/hakurei/ebf083d1b175911782d413369b64ce7c/pulse", "/run/user/65534/pulse/native", 0).
|
||||
Place(hst.Tmp+"/pulse-cookie", nil).
|
||||
Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/bus", "/run/user/65534/bus", 0).
|
||||
Bind("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/system_bus_socket", "/run/dbus/system_bus_socket", 0).
|
||||
Tmpfs("/var/run/nscd", 8192, 0755),
|
||||
SeccompPresets: seccomp.PresetExt | seccomp.PresetDenyDevel,
|
||||
HostNet: true,
|
||||
RetainSession: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"os/user"
|
||||
"strconv"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/internal/app"
|
||||
"hakurei.app/hst"
|
||||
)
|
||||
|
||||
// fs methods are not implemented using a real FS
|
||||
@@ -20,7 +20,7 @@ type stubNixOS struct {
|
||||
func (s *stubNixOS) Getuid() int { return 1971 }
|
||||
func (s *stubNixOS) Getgid() int { return 100 }
|
||||
func (s *stubNixOS) TempDir() string { return "/tmp" }
|
||||
func (s *stubNixOS) MustExecutable() string { return "/run/wrappers/bin/fortify" }
|
||||
func (s *stubNixOS) MustExecutable() string { return "/run/wrappers/bin/hakurei" }
|
||||
func (s *stubNixOS) Exit(code int) { panic("called exit on stub with code " + strconv.Itoa(code)) }
|
||||
func (s *stubNixOS) EvalSymlinks(path string) (string, error) { return path, nil }
|
||||
func (s *stubNixOS) Uid(aid int) (int, error) { return 1000000 + 0*10000 + aid, nil }
|
||||
@@ -125,10 +125,10 @@ func (s *stubNixOS) Open(name string) (fs.File, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stubNixOS) Paths() app.Paths {
|
||||
return app.Paths{
|
||||
SharePath: "/tmp/fortify.1971",
|
||||
func (s *stubNixOS) Paths() hst.Paths {
|
||||
return hst.Paths{
|
||||
SharePath: "/tmp/hakurei.1971",
|
||||
RuntimePath: "/run/user/1971",
|
||||
RunDirPath: "/run/user/1971/fortify",
|
||||
RunDirPath: "/run/user/1971/hakurei",
|
||||
}
|
||||
}
|
||||
104
cmd/hakurei/internal/app/internal/setuid/app_test.go
Normal file
104
cmd/hakurei/internal/app/internal/setuid/app_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package setuid_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/fs"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/cmd/hakurei/internal/app/internal/setuid"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/sys"
|
||||
"hakurei.app/system"
|
||||
)
|
||||
|
||||
type sealTestCase struct {
|
||||
name string
|
||||
os sys.State
|
||||
config *hst.Config
|
||||
id app.ID
|
||||
wantSys *system.I
|
||||
wantContainer *container.Params
|
||||
}
|
||||
|
||||
func TestApp(t *testing.T) {
|
||||
testCases := append(testCasesPd, testCasesNixos...)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
a := setuid.NewWithID(tc.id, tc.os)
|
||||
var (
|
||||
gotSys *system.I
|
||||
gotContainer *container.Params
|
||||
)
|
||||
if !t.Run("seal", func(t *testing.T) {
|
||||
if sa, err := a.Seal(tc.config); err != nil {
|
||||
t.Errorf("Seal: error = %v", err)
|
||||
return
|
||||
} else {
|
||||
gotSys, gotContainer = setuid.AppIParams(a, sa)
|
||||
}
|
||||
}) {
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("compare sys", func(t *testing.T) {
|
||||
if !gotSys.Equal(tc.wantSys) {
|
||||
t.Errorf("Seal: sys = %#v, want %#v",
|
||||
gotSys, tc.wantSys)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("compare params", func(t *testing.T) {
|
||||
if !reflect.DeepEqual(gotContainer, tc.wantContainer) {
|
||||
t.Errorf("seal: params =\n%s\n, want\n%s",
|
||||
mustMarshal(gotContainer), mustMarshal(tc.wantContainer))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustMarshal(v any) string {
|
||||
if b, err := json.Marshal(v); err != nil {
|
||||
panic(err.Error())
|
||||
} else {
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
func stubDirEntries(names ...string) (e []fs.DirEntry, err error) {
|
||||
e = make([]fs.DirEntry, len(names))
|
||||
for i, name := range names {
|
||||
e[i] = stubDirEntryPath(name)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type stubDirEntryPath string
|
||||
|
||||
func (p stubDirEntryPath) Name() string { return string(p) }
|
||||
func (p stubDirEntryPath) IsDir() bool { panic("attempted to call IsDir") }
|
||||
func (p stubDirEntryPath) Type() fs.FileMode { panic("attempted to call Type") }
|
||||
func (p stubDirEntryPath) Info() (fs.FileInfo, error) { panic("attempted to call Info") }
|
||||
|
||||
type stubFileInfoMode fs.FileMode
|
||||
|
||||
func (s stubFileInfoMode) Name() string { panic("attempted to call Name") }
|
||||
func (s stubFileInfoMode) Size() int64 { panic("attempted to call Size") }
|
||||
func (s stubFileInfoMode) Mode() fs.FileMode { return fs.FileMode(s) }
|
||||
func (s stubFileInfoMode) ModTime() time.Time { panic("attempted to call ModTime") }
|
||||
func (s stubFileInfoMode) IsDir() bool { panic("attempted to call IsDir") }
|
||||
func (s stubFileInfoMode) Sys() any { panic("attempted to call Sys") }
|
||||
|
||||
type stubFileInfoIsDir bool
|
||||
|
||||
func (s stubFileInfoIsDir) Name() string { panic("attempted to call Name") }
|
||||
func (s stubFileInfoIsDir) Size() int64 { panic("attempted to call Size") }
|
||||
func (s stubFileInfoIsDir) Mode() fs.FileMode { panic("attempted to call Mode") }
|
||||
func (s stubFileInfoIsDir) ModTime() time.Time { panic("attempted to call ModTime") }
|
||||
func (s stubFileInfoIsDir) IsDir() bool { return bool(s) }
|
||||
func (s stubFileInfoIsDir) Sys() any { panic("attempted to call Sys") }
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"errors"
|
||||
"log"
|
||||
|
||||
. "git.gensokyo.uk/security/fortify/internal/app"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
. "hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
func PrintRunStateErr(rs *RunState, runErr error) (code int) {
|
||||
@@ -13,10 +13,10 @@ func PrintRunStateErr(rs *RunState, runErr error) (code int) {
|
||||
|
||||
if runErr != nil {
|
||||
if rs.Time == nil {
|
||||
fmsg.PrintBaseError(runErr, "cannot start app:")
|
||||
hlog.PrintBaseError(runErr, "cannot start app:")
|
||||
} else {
|
||||
var e *fmsg.BaseError
|
||||
if !fmsg.AsBaseError(runErr, &e) {
|
||||
var e *hlog.BaseError
|
||||
if !hlog.AsBaseError(runErr, &e) {
|
||||
log.Println("wait failed:", runErr)
|
||||
} else {
|
||||
// Wait only returns either *app.ProcessError or *app.StateStoreError wrapped in a *app.BaseError
|
||||
@@ -37,7 +37,7 @@ func PrintRunStateErr(rs *RunState, runErr error) (code int) {
|
||||
|
||||
// every error here is wrapped in *app.BaseError
|
||||
for _, ei := range errs {
|
||||
var eb *fmsg.BaseError
|
||||
var eb *hlog.BaseError
|
||||
if !errors.As(ei, &eb) {
|
||||
// unreachable
|
||||
log.Println("invalid error type returned by revert:", ei)
|
||||
@@ -59,7 +59,7 @@ func PrintRunStateErr(rs *RunState, runErr error) (code int) {
|
||||
if rs.RevertErr != nil {
|
||||
var stateStoreError *StateStoreError
|
||||
if !errors.As(rs.RevertErr, &stateStoreError) || stateStoreError == nil {
|
||||
fmsg.PrintBaseError(rs.RevertErr, "generic fault during cleanup:")
|
||||
hlog.PrintBaseError(rs.RevertErr, "generic fault during cleanup:")
|
||||
goto out
|
||||
}
|
||||
|
||||
@@ -67,11 +67,11 @@ func PrintRunStateErr(rs *RunState, runErr error) (code int) {
|
||||
if len(stateStoreError.Err) == 2 {
|
||||
if stateStoreError.Err[0] != nil {
|
||||
if joinedErrs, ok := stateStoreError.Err[0].(interface{ Unwrap() []error }); !ok {
|
||||
fmsg.PrintBaseError(stateStoreError.Err[0], "generic fault during revert:")
|
||||
hlog.PrintBaseError(stateStoreError.Err[0], "generic fault during revert:")
|
||||
} else {
|
||||
for _, err := range joinedErrs.Unwrap() {
|
||||
if err != nil {
|
||||
fmsg.PrintBaseError(err, "fault during revert:")
|
||||
hlog.PrintBaseError(err, "fault during revert:")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,11 +91,11 @@ func PrintRunStateErr(rs *RunState, runErr error) (code int) {
|
||||
}
|
||||
|
||||
if stateStoreError.DoErr != nil {
|
||||
fmsg.PrintBaseError(stateStoreError.DoErr, "state store operation unsuccessful:")
|
||||
hlog.PrintBaseError(stateStoreError.DoErr, "state store operation unsuccessful:")
|
||||
}
|
||||
|
||||
if stateStoreError.Inner && stateStoreError.InnerErr != nil {
|
||||
fmsg.PrintBaseError(stateStoreError.InnerErr, "cannot destroy state entry:")
|
||||
hlog.PrintBaseError(stateStoreError.InnerErr, "cannot destroy state entry:")
|
||||
}
|
||||
|
||||
out:
|
||||
@@ -104,7 +104,7 @@ func PrintRunStateErr(rs *RunState, runErr error) (code int) {
|
||||
}
|
||||
}
|
||||
if rs.WaitErr != nil {
|
||||
fmsg.Verbosef("wait: %v", rs.WaitErr)
|
||||
hlog.Verbosef("wait: %v", rs.WaitErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -135,7 +135,7 @@ func (e *StateStoreError) equiv(a ...any) error {
|
||||
if e.Inner && e.InnerErr == nil && e.DoErr == nil && e.OpErr == nil && errors.Join(e.Err...) == nil {
|
||||
return nil
|
||||
} else {
|
||||
return fmsg.WrapErrorSuffix(e, a...)
|
||||
return hlog.WrapErrSuffix(e, a...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package setuid
|
||||
|
||||
import (
|
||||
. "git.gensokyo.uk/security/fortify/internal/app"
|
||||
"git.gensokyo.uk/security/fortify/internal/sys"
|
||||
"git.gensokyo.uk/security/fortify/sandbox"
|
||||
"git.gensokyo.uk/security/fortify/system"
|
||||
. "hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/internal/sys"
|
||||
"hakurei.app/system"
|
||||
)
|
||||
|
||||
func NewWithID(id ID, os sys.State) App {
|
||||
@@ -14,7 +14,7 @@ func NewWithID(id ID, os sys.State) App {
|
||||
return a
|
||||
}
|
||||
|
||||
func AppIParams(a App, sa SealedApp) (*system.I, *sandbox.Params) {
|
||||
func AppIParams(a App, sa SealedApp) (*system.I, *container.Params) {
|
||||
v := a.(*app)
|
||||
seal := sa.(*outcome)
|
||||
if v.outcome != seal || v.id != seal.id {
|
||||
@@ -12,12 +12,12 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
. "git.gensokyo.uk/security/fortify/internal/app"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"git.gensokyo.uk/security/fortify/internal/state"
|
||||
"git.gensokyo.uk/security/fortify/sandbox"
|
||||
"git.gensokyo.uk/security/fortify/system"
|
||||
. "hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/cmd/hakurei/internal/state"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/system"
|
||||
)
|
||||
|
||||
const shimWaitTimeout = 5 * time.Second
|
||||
@@ -35,7 +35,7 @@ func (seal *outcome) Run(rs *RunState) error {
|
||||
}
|
||||
|
||||
// read comp value early to allow for early failure
|
||||
fsuPath := internal.MustFsuPath()
|
||||
hsuPath := internal.MustHsuPath()
|
||||
|
||||
if err := seal.sys.Commit(seal.ctx); err != nil {
|
||||
return err
|
||||
@@ -59,7 +59,7 @@ func (seal *outcome) Run(rs *RunState) error {
|
||||
if l := len(states); l == 0 {
|
||||
ec |= system.User
|
||||
} else {
|
||||
fmsg.Verbosef("found %d instances, cleaning up without user-scoped operations", l)
|
||||
hlog.Verbosef("found %d instances, cleaning up without user-scoped operations", l)
|
||||
}
|
||||
|
||||
// accumulate enablements of remaining launchers
|
||||
@@ -72,9 +72,9 @@ func (seal *outcome) Run(rs *RunState) error {
|
||||
}
|
||||
}
|
||||
ec |= rt ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse)
|
||||
if fmsg.Load() {
|
||||
if hlog.Load() {
|
||||
if ec > 0 {
|
||||
fmsg.Verbose("reverting operations scope", system.TypeString(ec))
|
||||
hlog.Verbose("reverting operations scope", system.TypeString(ec))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,36 +87,36 @@ func (seal *outcome) Run(rs *RunState) error {
|
||||
|
||||
ctx, cancel := context.WithCancel(seal.ctx)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, fsuPath)
|
||||
cmd := exec.CommandContext(ctx, hsuPath)
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
cmd.Dir = "/" // container init enters final working directory
|
||||
// shim runs in the same session as monitor; see shim.go for behaviour
|
||||
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) }
|
||||
|
||||
var e *gob.Encoder
|
||||
if fd, encoder, err := sandbox.Setup(&cmd.ExtraFiles); err != nil {
|
||||
return fmsg.WrapErrorSuffix(err,
|
||||
if fd, encoder, err := container.Setup(&cmd.ExtraFiles); err != nil {
|
||||
return hlog.WrapErrSuffix(err,
|
||||
"cannot create shim setup pipe:")
|
||||
} else {
|
||||
e = encoder
|
||||
cmd.Env = []string{
|
||||
// passed through to shim by fsu
|
||||
// passed through to shim by hsu
|
||||
shimEnv + "=" + strconv.Itoa(fd),
|
||||
// interpreted by fsu
|
||||
"FORTIFY_APP_ID=" + seal.user.aid.String(),
|
||||
// interpreted by hsu
|
||||
"HAKUREI_APP_ID=" + seal.user.aid.String(),
|
||||
}
|
||||
}
|
||||
|
||||
if len(seal.user.supp) > 0 {
|
||||
fmsg.Verbosef("attaching supplementary group ids %s", seal.user.supp)
|
||||
// interpreted by fsu
|
||||
cmd.Env = append(cmd.Env, "FORTIFY_GROUPS="+strings.Join(seal.user.supp, " "))
|
||||
hlog.Verbosef("attaching supplementary group ids %s", seal.user.supp)
|
||||
// interpreted by hsu
|
||||
cmd.Env = append(cmd.Env, "HAKUREI_GROUPS="+strings.Join(seal.user.supp, " "))
|
||||
}
|
||||
|
||||
fmsg.Verbosef("setuid helper at %s", fsuPath)
|
||||
fmsg.Suspend()
|
||||
hlog.Verbosef("setuid helper at %s", hsuPath)
|
||||
hlog.Suspend()
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmsg.WrapErrorSuffix(err,
|
||||
return hlog.WrapErrSuffix(err,
|
||||
"cannot start setuid wrapper:")
|
||||
}
|
||||
rs.SetStart()
|
||||
@@ -124,19 +124,19 @@ func (seal *outcome) Run(rs *RunState) error {
|
||||
// this prevents blocking forever on an early failure
|
||||
waitErr, setupErr := make(chan error, 1), make(chan error, 1)
|
||||
go func() { waitErr <- cmd.Wait(); cancel() }()
|
||||
go func() { setupErr <- e.Encode(&shimParams{os.Getpid(), seal.container, seal.user.data, fmsg.Load()}) }()
|
||||
go func() { setupErr <- e.Encode(&shimParams{os.Getpid(), seal.container, seal.user.data, hlog.Load()}) }()
|
||||
|
||||
select {
|
||||
case err := <-setupErr:
|
||||
if err != nil {
|
||||
fmsg.Resume()
|
||||
return fmsg.WrapErrorSuffix(err,
|
||||
hlog.Resume()
|
||||
return hlog.WrapErrSuffix(err,
|
||||
"cannot transmit shim config:")
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
fmsg.Resume()
|
||||
return fmsg.WrapError(syscall.ECANCELED,
|
||||
hlog.Resume()
|
||||
return hlog.WrapErr(syscall.ECANCELED,
|
||||
"shim setup canceled")
|
||||
}
|
||||
|
||||
@@ -163,25 +163,25 @@ func (seal *outcome) Run(rs *RunState) error {
|
||||
select {
|
||||
case rs.WaitErr = <-waitErr:
|
||||
rs.WaitStatus = cmd.ProcessState.Sys().(syscall.WaitStatus)
|
||||
if fmsg.Load() {
|
||||
if hlog.Load() {
|
||||
switch {
|
||||
case rs.Exited():
|
||||
fmsg.Verbosef("process %d exited with code %d", cmd.Process.Pid, rs.ExitStatus())
|
||||
hlog.Verbosef("process %d exited with code %d", cmd.Process.Pid, rs.ExitStatus())
|
||||
case rs.CoreDump():
|
||||
fmsg.Verbosef("process %d dumped core", cmd.Process.Pid)
|
||||
hlog.Verbosef("process %d dumped core", cmd.Process.Pid)
|
||||
case rs.Signaled():
|
||||
fmsg.Verbosef("process %d got %s", cmd.Process.Pid, rs.Signal())
|
||||
hlog.Verbosef("process %d got %s", cmd.Process.Pid, rs.Signal())
|
||||
default:
|
||||
fmsg.Verbosef("process %d exited with status %#x", cmd.Process.Pid, rs.WaitStatus)
|
||||
hlog.Verbosef("process %d exited with status %#x", cmd.Process.Pid, rs.WaitStatus)
|
||||
}
|
||||
}
|
||||
case <-waitTimeout:
|
||||
rs.WaitErr = syscall.ETIMEDOUT
|
||||
fmsg.Resume()
|
||||
hlog.Resume()
|
||||
log.Printf("process %d did not terminate", cmd.Process.Pid)
|
||||
}
|
||||
|
||||
fmsg.Resume()
|
||||
hlog.Resume()
|
||||
if seal.sync != nil {
|
||||
if err := seal.sync.Close(); err != nil {
|
||||
log.Printf("cannot close wayland security context: %v", err)
|
||||
@@ -16,17 +16,17 @@ import (
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/acl"
|
||||
"git.gensokyo.uk/security/fortify/dbus"
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
. "git.gensokyo.uk/security/fortify/internal/app"
|
||||
"git.gensokyo.uk/security/fortify/internal/app/instance/common"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"git.gensokyo.uk/security/fortify/internal/sys"
|
||||
"git.gensokyo.uk/security/fortify/sandbox"
|
||||
"git.gensokyo.uk/security/fortify/sandbox/wl"
|
||||
"git.gensokyo.uk/security/fortify/system"
|
||||
. "hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/cmd/hakurei/internal/app/instance/common"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/internal/sys"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/acl"
|
||||
"hakurei.app/system/dbus"
|
||||
"hakurei.app/system/wayland"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -63,24 +63,24 @@ var (
|
||||
|
||||
var posixUsername = regexp.MustCompilePOSIX("^[a-z_]([A-Za-z0-9_-]{0,31}|[A-Za-z0-9_-]{0,30}\\$)$")
|
||||
|
||||
// outcome stores copies of various parts of [fst.Config]
|
||||
// outcome stores copies of various parts of [hst.Config]
|
||||
type outcome struct {
|
||||
// copied from initialising [app]
|
||||
id *stringPair[ID]
|
||||
// copied from [sys.State] response
|
||||
runDirPath string
|
||||
|
||||
// initial [fst.Config] gob stream for state data;
|
||||
// initial [hst.Config] gob stream for state data;
|
||||
// this is prepared ahead of time as config is clobbered during seal creation
|
||||
ct io.WriterTo
|
||||
// dump dbus proxy message buffer
|
||||
dbusMsg func()
|
||||
|
||||
user fsuUser
|
||||
user hsuUser
|
||||
sys *system.I
|
||||
ctx context.Context
|
||||
|
||||
container *sandbox.Params
|
||||
container *container.Params
|
||||
env map[string]string
|
||||
sync *os.File
|
||||
|
||||
@@ -89,7 +89,7 @@ type outcome struct {
|
||||
|
||||
// shareHost holds optional share directory state that must not be accessed directly
|
||||
type shareHost struct {
|
||||
// whether XDG_RUNTIME_DIR is used post fsu
|
||||
// whether XDG_RUNTIME_DIR is used post hsu
|
||||
useRuntimeDir bool
|
||||
// process-specific directory in tmpdir, empty if unused
|
||||
sharePath string
|
||||
@@ -97,7 +97,7 @@ type shareHost struct {
|
||||
runtimeSharePath string
|
||||
|
||||
seal *outcome
|
||||
sc Paths
|
||||
sc hst.Paths
|
||||
}
|
||||
|
||||
// ensureRuntimeDir must be called if direct access to paths within XDG_RUNTIME_DIR is required
|
||||
@@ -134,8 +134,8 @@ func (share *shareHost) runtime() string {
|
||||
return share.runtimeSharePath
|
||||
}
|
||||
|
||||
// fsuUser stores post-fsu credentials and metadata
|
||||
type fsuUser struct {
|
||||
// hsuUser stores post-hsu credentials and metadata
|
||||
type hsuUser struct {
|
||||
// application id
|
||||
aid *stringPair[int]
|
||||
// target uid resolved by fid:aid
|
||||
@@ -152,7 +152,7 @@ type fsuUser struct {
|
||||
username string
|
||||
}
|
||||
|
||||
func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Config) error {
|
||||
func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *hst.Config) error {
|
||||
if seal.ctx != nil {
|
||||
panic("finalise called twice")
|
||||
}
|
||||
@@ -162,19 +162,19 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
||||
// encode initial configuration for state tracking
|
||||
ct := new(bytes.Buffer)
|
||||
if err := gob.NewEncoder(ct).Encode(config); err != nil {
|
||||
return fmsg.WrapErrorSuffix(err,
|
||||
return hlog.WrapErrSuffix(err,
|
||||
"cannot encode initial config:")
|
||||
}
|
||||
seal.ct = ct
|
||||
}
|
||||
|
||||
// allowed aid range 0 to 9999, this is checked again in fsu
|
||||
// allowed aid range 0 to 9999, this is checked again in hsu
|
||||
if config.Identity < 0 || config.Identity > 9999 {
|
||||
return fmsg.WrapError(ErrUser,
|
||||
return hlog.WrapErr(ErrUser,
|
||||
fmt.Sprintf("identity %d out of range", config.Identity))
|
||||
}
|
||||
|
||||
seal.user = fsuUser{
|
||||
seal.user = hsuUser{
|
||||
aid: newInt(config.Identity),
|
||||
data: config.Data,
|
||||
home: config.Dir,
|
||||
@@ -183,12 +183,12 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
||||
if seal.user.username == "" {
|
||||
seal.user.username = "chronos"
|
||||
} else if !posixUsername.MatchString(seal.user.username) ||
|
||||
len(seal.user.username) >= internal.Sysconf_SC_LOGIN_NAME_MAX() {
|
||||
return fmsg.WrapError(ErrName,
|
||||
len(seal.user.username) >= internal.Sysconf(internal.SC_LOGIN_NAME_MAX) {
|
||||
return hlog.WrapErr(ErrName,
|
||||
fmt.Sprintf("invalid user name %q", seal.user.username))
|
||||
}
|
||||
if seal.user.data == "" || !path.IsAbs(seal.user.data) {
|
||||
return fmsg.WrapError(ErrHome,
|
||||
return hlog.WrapErr(ErrHome,
|
||||
fmt.Sprintf("invalid home directory %q", seal.user.data))
|
||||
}
|
||||
if seal.user.home == "" {
|
||||
@@ -202,7 +202,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
||||
seal.user.supp = make([]string, len(config.Groups))
|
||||
for i, name := range config.Groups {
|
||||
if g, err := sys.LookupGroup(name); err != nil {
|
||||
return fmsg.WrapError(err,
|
||||
return hlog.WrapErr(err,
|
||||
fmt.Sprintf("unknown group %q", name))
|
||||
} else {
|
||||
seal.user.supp[i] = g.Gid
|
||||
@@ -220,13 +220,13 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
||||
|
||||
// permissive defaults
|
||||
if config.Container == nil {
|
||||
fmsg.Verbose("container configuration not supplied, PROCEED WITH CAUTION")
|
||||
hlog.Verbose("container configuration not supplied, PROCEED WITH CAUTION")
|
||||
|
||||
// fsu clears the environment so resolve paths early
|
||||
// hsu clears the environment so resolve paths early
|
||||
if !path.IsAbs(config.Path) {
|
||||
if len(config.Args) > 0 {
|
||||
if p, err := sys.LookPath(config.Args[0]); err != nil {
|
||||
return fmsg.WrapError(err, err.Error())
|
||||
return hlog.WrapErr(err, err.Error())
|
||||
} else {
|
||||
config.Path = p
|
||||
}
|
||||
@@ -235,7 +235,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
||||
}
|
||||
}
|
||||
|
||||
conf := &fst.ContainerConfig{
|
||||
conf := &hst.ContainerConfig{
|
||||
Userns: true,
|
||||
Net: true,
|
||||
Tty: true,
|
||||
@@ -245,7 +245,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
||||
if d, err := sys.ReadDir("/"); err != nil {
|
||||
return err
|
||||
} else {
|
||||
b := make([]*fst.FilesystemConfig, 0, len(d))
|
||||
b := make([]*hst.FilesystemConfig, 0, len(d))
|
||||
for _, ent := range d {
|
||||
p := "/" + ent.Name()
|
||||
switch p {
|
||||
@@ -256,7 +256,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
||||
case "/etc":
|
||||
|
||||
default:
|
||||
b = append(b, &fst.FilesystemConfig{Src: p, Write: true, Must: true})
|
||||
b = append(b, &hst.FilesystemConfig{Src: p, Write: true, Must: true})
|
||||
}
|
||||
}
|
||||
conf.Filesystem = append(conf.Filesystem, b...)
|
||||
@@ -269,10 +269,10 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
||||
}
|
||||
// bind GPU stuff
|
||||
if config.Enablements&(system.EX11|system.EWayland) != 0 {
|
||||
conf.Filesystem = append(conf.Filesystem, &fst.FilesystemConfig{Src: "/dev/dri", Device: true})
|
||||
conf.Filesystem = append(conf.Filesystem, &hst.FilesystemConfig{Src: "/dev/dri", Device: true})
|
||||
}
|
||||
// opportunistically bind kvm
|
||||
conf.Filesystem = append(conf.Filesystem, &fst.FilesystemConfig{Src: "/dev/kvm", Device: true})
|
||||
conf.Filesystem = append(conf.Filesystem, &hst.FilesystemConfig{Src: "/dev/kvm", Device: true})
|
||||
|
||||
config.Container = conf
|
||||
}
|
||||
@@ -283,11 +283,11 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
||||
var err error
|
||||
seal.container, seal.env, err = common.NewContainer(config.Container, sys, &uid, &gid)
|
||||
if err != nil {
|
||||
return fmsg.WrapErrorSuffix(err,
|
||||
return hlog.WrapErrSuffix(err,
|
||||
"cannot initialise container configuration:")
|
||||
}
|
||||
if !path.IsAbs(config.Path) {
|
||||
return fmsg.WrapError(syscall.EINVAL,
|
||||
return hlog.WrapErr(syscall.EINVAL,
|
||||
"invalid program path")
|
||||
}
|
||||
if len(config.Args) == 0 {
|
||||
@@ -317,8 +317,6 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
||||
|
||||
// inner XDG_RUNTIME_DIR default formatting of `/run/user/%d` as mapped uid
|
||||
innerRuntimeDir := path.Join("/run/user", mapuid.String())
|
||||
seal.container.Tmpfs("/run/user", 1<<12, 0755)
|
||||
seal.container.Tmpfs(innerRuntimeDir, 1<<23, 0700)
|
||||
seal.env[xdgRuntimeDir] = innerRuntimeDir
|
||||
seal.env[xdgSessionClass] = "user"
|
||||
seal.env[xdgSessionType] = "tty"
|
||||
@@ -326,9 +324,20 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
||||
share := &shareHost{seal: seal, sc: sys.Paths()}
|
||||
seal.runDirPath = share.sc.RunDirPath
|
||||
seal.sys = system.New(seal.user.uid.unwrap())
|
||||
seal.sys.Ensure(share.sc.SharePath, 0711)
|
||||
|
||||
{
|
||||
runtimeDir := path.Join(share.sc.SharePath, "runtime")
|
||||
seal.sys.Ensure(runtimeDir, 0700)
|
||||
seal.sys.UpdatePermType(system.User, runtimeDir, acl.Execute)
|
||||
runtimeDirInst := path.Join(runtimeDir, seal.user.aid.String())
|
||||
seal.sys.Ensure(runtimeDirInst, 0700)
|
||||
seal.sys.UpdatePermType(system.User, runtimeDirInst, acl.Read, acl.Write, acl.Execute)
|
||||
seal.container.Tmpfs("/run/user", 1<<12, 0755)
|
||||
seal.container.Bind(runtimeDirInst, innerRuntimeDir, container.BindWritable)
|
||||
}
|
||||
|
||||
{
|
||||
seal.sys.Ensure(share.sc.SharePath, 0711)
|
||||
tmpdir := path.Join(share.sc.SharePath, "tmpdir")
|
||||
seal.sys.Ensure(tmpdir, 0700)
|
||||
seal.sys.UpdatePermType(system.User, tmpdir, acl.Execute)
|
||||
@@ -336,7 +345,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
||||
seal.sys.Ensure(tmpdirInst, 01700)
|
||||
seal.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute)
|
||||
// mount inner /tmp from share so it shares persistence and storage behaviour of host /tmp
|
||||
seal.container.Bind(tmpdirInst, "/tmp", sandbox.BindWritable)
|
||||
seal.container.Bind(tmpdirInst, "/tmp", container.BindWritable)
|
||||
}
|
||||
|
||||
{
|
||||
@@ -348,16 +357,16 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
||||
if seal.user.username != "" {
|
||||
username = seal.user.username
|
||||
}
|
||||
seal.container.Bind(seal.user.data, homeDir, sandbox.BindWritable)
|
||||
seal.container.Bind(seal.user.data, homeDir, container.BindWritable)
|
||||
seal.container.Dir = homeDir
|
||||
seal.env["HOME"] = homeDir
|
||||
seal.env["USER"] = username
|
||||
seal.env[shell] = config.Shell
|
||||
|
||||
seal.container.Place("/etc/passwd",
|
||||
[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Fortify:"+homeDir+":"+config.Shell+"\n"))
|
||||
[]byte(username+":x:"+mapuid.String()+":"+mapgid.String()+":Hakurei:"+homeDir+":"+config.Shell+"\n"))
|
||||
seal.container.Place("/etc/group",
|
||||
[]byte("fortify:x:"+mapgid.String()+":\n"))
|
||||
[]byte("hakurei:x:"+mapgid.String()+":\n"))
|
||||
}
|
||||
|
||||
// pass TERM for proper terminal I/O in initial process
|
||||
@@ -368,30 +377,30 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
||||
if config.Enablements&system.EWayland != 0 {
|
||||
// outer wayland socket (usually `/run/user/%d/wayland-%d`)
|
||||
var socketPath string
|
||||
if name, ok := sys.LookupEnv(wl.WaylandDisplay); !ok {
|
||||
fmsg.Verbose(wl.WaylandDisplay + " is not set, assuming " + wl.FallbackName)
|
||||
socketPath = path.Join(share.sc.RuntimePath, wl.FallbackName)
|
||||
if name, ok := sys.LookupEnv(wayland.WaylandDisplay); !ok {
|
||||
hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName)
|
||||
socketPath = path.Join(share.sc.RuntimePath, wayland.FallbackName)
|
||||
} else if !path.IsAbs(name) {
|
||||
socketPath = path.Join(share.sc.RuntimePath, name)
|
||||
} else {
|
||||
socketPath = name
|
||||
}
|
||||
|
||||
innerPath := path.Join(innerRuntimeDir, wl.FallbackName)
|
||||
seal.env[wl.WaylandDisplay] = wl.FallbackName
|
||||
innerPath := path.Join(innerRuntimeDir, wayland.FallbackName)
|
||||
seal.env[wayland.WaylandDisplay] = wayland.FallbackName
|
||||
|
||||
if !config.DirectWayland { // set up security-context-v1
|
||||
appID := config.ID
|
||||
if appID == "" {
|
||||
// use instance ID in case app id is not set
|
||||
appID = "uk.gensokyo.fortify." + seal.id.String()
|
||||
appID = "app.hakurei." + seal.id.String()
|
||||
}
|
||||
// downstream socket paths
|
||||
outerPath := path.Join(share.instance(), "wayland")
|
||||
seal.sys.Wayland(&seal.sync, outerPath, socketPath, appID, seal.id.String())
|
||||
seal.container.Bind(outerPath, innerPath, 0)
|
||||
} else { // bind mount wayland socket (insecure)
|
||||
fmsg.Verbose("direct wayland access, PROCEED WITH CAUTION")
|
||||
hlog.Verbose("direct wayland access, PROCEED WITH CAUTION")
|
||||
share.ensureRuntimeDir()
|
||||
seal.container.Bind(socketPath, innerPath, 0)
|
||||
seal.sys.UpdatePermType(system.EWayland, socketPath, acl.Read, acl.Write, acl.Execute)
|
||||
@@ -400,7 +409,7 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
||||
|
||||
if config.Enablements&system.EX11 != 0 {
|
||||
if d, ok := sys.LookupEnv(display); !ok {
|
||||
return fmsg.WrapError(ErrXDisplay,
|
||||
return hlog.WrapErr(ErrXDisplay,
|
||||
"DISPLAY is not set")
|
||||
} else {
|
||||
seal.sys.ChangeHosts("#" + seal.user.uid.String())
|
||||
@@ -417,23 +426,23 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
||||
|
||||
if _, err := sys.Stat(pulseRuntimeDir); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return fmsg.WrapErrorSuffix(err,
|
||||
return hlog.WrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot access PulseAudio directory %q:", pulseRuntimeDir))
|
||||
}
|
||||
return fmsg.WrapError(ErrPulseSocket,
|
||||
return hlog.WrapErr(ErrPulseSocket,
|
||||
fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir))
|
||||
}
|
||||
|
||||
if s, err := sys.Stat(pulseSocket); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return fmsg.WrapErrorSuffix(err,
|
||||
return hlog.WrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot access PulseAudio socket %q:", pulseSocket))
|
||||
}
|
||||
return fmsg.WrapError(ErrPulseSocket,
|
||||
return hlog.WrapErr(ErrPulseSocket,
|
||||
fmt.Sprintf("PulseAudio directory %q found but socket does not exist", pulseRuntimeDir))
|
||||
} else {
|
||||
if m := s.Mode(); m&0o006 != 0o006 {
|
||||
return fmsg.WrapError(ErrPulseMode,
|
||||
return hlog.WrapErr(ErrPulseMode,
|
||||
fmt.Sprintf("unexpected permissions on %q:", pulseSocket), m)
|
||||
}
|
||||
}
|
||||
@@ -448,9 +457,9 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
||||
// publish current user's pulse cookie for target user
|
||||
if src, err := discoverPulseCookie(sys); err != nil {
|
||||
// not fatal
|
||||
fmsg.Verbose(strings.TrimSpace(err.(*fmsg.BaseError).Message()))
|
||||
hlog.Verbose(strings.TrimSpace(err.(*hlog.BaseError).Message()))
|
||||
} else {
|
||||
innerDst := fst.Tmp + "/pulse-cookie"
|
||||
innerDst := hst.Tmp + "/pulse-cookie"
|
||||
seal.env[pulseCookie] = innerDst
|
||||
var payload *[]byte
|
||||
seal.container.PlaceP(innerDst, &payload)
|
||||
@@ -522,15 +531,15 @@ func (seal *outcome) finalise(ctx context.Context, sys sys.State, config *fst.Co
|
||||
seal.container.Env = make([]string, 0, len(seal.env))
|
||||
for k, v := range seal.env {
|
||||
if strings.IndexByte(k, '=') != -1 {
|
||||
return fmsg.WrapError(syscall.EINVAL,
|
||||
return hlog.WrapErr(syscall.EINVAL,
|
||||
fmt.Sprintf("invalid environment variable %s", k))
|
||||
}
|
||||
seal.container.Env = append(seal.container.Env, k+"="+v)
|
||||
}
|
||||
slices.Sort(seal.container.Env)
|
||||
|
||||
if fmsg.Load() {
|
||||
fmsg.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s, ops: %d",
|
||||
if hlog.Load() {
|
||||
hlog.Verbosef("created application seal for uid %s (%s) groups: %v, argv: %s, ops: %d",
|
||||
seal.user.uid, seal.user.username, config.Groups, seal.container.Args, len(*seal.container.Ops))
|
||||
}
|
||||
|
||||
@@ -548,7 +557,7 @@ func discoverPulseCookie(sys sys.State) (string, error) {
|
||||
p = path.Join(p, ".pulse-cookie")
|
||||
if s, err := sys.Stat(p); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return p, fmsg.WrapErrorSuffix(err,
|
||||
return p, hlog.WrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
|
||||
}
|
||||
// not found, try next method
|
||||
@@ -562,7 +571,7 @@ func discoverPulseCookie(sys sys.State) (string, error) {
|
||||
p = path.Join(p, "pulse", "cookie")
|
||||
if s, err := sys.Stat(p); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return p, fmsg.WrapErrorSuffix(err,
|
||||
return p, hlog.WrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot access PulseAudio cookie %q:", p))
|
||||
}
|
||||
// not found, try next method
|
||||
@@ -571,7 +580,7 @@ func discoverPulseCookie(sys sys.State) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmsg.WrapError(ErrPulseCookie,
|
||||
return "", hlog.WrapErr(ErrPulseCookie,
|
||||
fmt.Sprintf("cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
|
||||
pulseCookie, xdgConfigHome, home))
|
||||
}
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"git.gensokyo.uk/security/fortify/sandbox"
|
||||
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -23,10 +23,10 @@ import (
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
|
||||
static pid_t f_shim_param_ppid = -1;
|
||||
static pid_t hakurei_shim_param_ppid = -1;
|
||||
|
||||
// this cannot unblock fmsg since Go code is not async-signal-safe
|
||||
static void f_shim_sigaction(int sig, siginfo_t *si, void *ucontext) {
|
||||
// this cannot unblock hlog since Go code is not async-signal-safe
|
||||
static void hakurei_shim_sigaction(int sig, siginfo_t *si, void *ucontext) {
|
||||
if (sig != SIGCONT || si == NULL) {
|
||||
// unreachable
|
||||
fprintf(stderr, "sigaction: sa_sigaction got invalid siginfo\n");
|
||||
@@ -34,17 +34,17 @@ static void f_shim_sigaction(int sig, siginfo_t *si, void *ucontext) {
|
||||
}
|
||||
|
||||
// monitor requests shim exit
|
||||
if (si->si_pid == f_shim_param_ppid)
|
||||
if (si->si_pid == hakurei_shim_param_ppid)
|
||||
exit(254);
|
||||
|
||||
fprintf(stderr, "sigaction: got SIGCONT from process %d\n", si->si_pid);
|
||||
|
||||
// shim orphaned before monitor delivers a signal
|
||||
if (getppid() != f_shim_param_ppid)
|
||||
if (getppid() != hakurei_shim_param_ppid)
|
||||
exit(3);
|
||||
}
|
||||
|
||||
void f_shim_setup_cont_signal(pid_t ppid) {
|
||||
void hakurei_shim_setup_cont_signal(pid_t ppid) {
|
||||
struct sigaction new_action = {0}, old_action = {0};
|
||||
if (sigaction(SIGCONT, NULL, &old_action) != 0)
|
||||
return;
|
||||
@@ -53,7 +53,7 @@ void f_shim_setup_cont_signal(pid_t ppid) {
|
||||
return;
|
||||
}
|
||||
|
||||
new_action.sa_sigaction = f_shim_sigaction;
|
||||
new_action.sa_sigaction = hakurei_shim_sigaction;
|
||||
if (sigemptyset(&new_action.sa_mask) != 0)
|
||||
return;
|
||||
new_action.sa_flags = SA_ONSTACK | SA_SIGINFO;
|
||||
@@ -62,19 +62,19 @@ void f_shim_setup_cont_signal(pid_t ppid) {
|
||||
return;
|
||||
|
||||
errno = 0;
|
||||
f_shim_param_ppid = ppid;
|
||||
hakurei_shim_param_ppid = ppid;
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
const shimEnv = "FORTIFY_SHIM"
|
||||
const shimEnv = "HAKUREI_SHIM"
|
||||
|
||||
type shimParams struct {
|
||||
// monitor pid, checked against ppid in signal handler
|
||||
Monitor int
|
||||
|
||||
// finalised container params
|
||||
Container *sandbox.Params
|
||||
Container *container.Params
|
||||
// path to outer home directory
|
||||
Home string
|
||||
|
||||
@@ -84,9 +84,9 @@ type shimParams struct {
|
||||
|
||||
// ShimMain is the main function of the shim process and runs as the unconstrained target user.
|
||||
func ShimMain() {
|
||||
fmsg.Prepare("shim")
|
||||
hlog.Prepare("shim")
|
||||
|
||||
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
|
||||
if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil {
|
||||
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||
}
|
||||
|
||||
@@ -94,21 +94,21 @@ func ShimMain() {
|
||||
params shimParams
|
||||
closeSetup func() error
|
||||
)
|
||||
if f, err := sandbox.Receive(shimEnv, ¶ms, nil); err != nil {
|
||||
if errors.Is(err, sandbox.ErrInvalid) {
|
||||
if f, err := container.Receive(shimEnv, ¶ms, nil); err != nil {
|
||||
if errors.Is(err, container.ErrInvalid) {
|
||||
log.Fatal("invalid config descriptor")
|
||||
}
|
||||
if errors.Is(err, sandbox.ErrNotSet) {
|
||||
log.Fatal("FORTIFY_SHIM not set")
|
||||
if errors.Is(err, container.ErrNotSet) {
|
||||
log.Fatal("HAKUREI_SHIM not set")
|
||||
}
|
||||
|
||||
log.Fatalf("cannot receive shim setup params: %v", err)
|
||||
} else {
|
||||
internal.InstallFmsg(params.Verbose)
|
||||
internal.InstallOutput(params.Verbose)
|
||||
closeSetup = f
|
||||
|
||||
// the Go runtime does not expose siginfo_t so SIGCONT is handled in C to check si_pid
|
||||
if _, err = C.f_shim_setup_cont_signal(C.pid_t(params.Monitor)); err != nil {
|
||||
if _, err = C.hakurei_shim_setup_cont_signal(C.pid_t(params.Monitor)); err != nil {
|
||||
log.Fatalf("cannot install SIGCONT handler: %v", err)
|
||||
}
|
||||
|
||||
@@ -149,25 +149,28 @@ func ShimMain() {
|
||||
}
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop() // unreachable
|
||||
container := sandbox.New(ctx, name)
|
||||
container.Params = *params.Container
|
||||
container.Stdin, container.Stdout, container.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
container.Cancel = func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
|
||||
container.WaitDelay = 2 * time.Second
|
||||
z := container.New(ctx, name)
|
||||
z.Params = *params.Container
|
||||
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
z.Cancel = func(cmd *exec.Cmd) error { return cmd.Process.Signal(os.Interrupt) }
|
||||
z.WaitDelay = 2 * time.Second
|
||||
|
||||
if err := container.Start(); err != nil {
|
||||
fmsg.PrintBaseError(err, "cannot start container:")
|
||||
if err := z.Start(); err != nil {
|
||||
hlog.PrintBaseError(err, "cannot start container:")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := container.Serve(); err != nil {
|
||||
fmsg.PrintBaseError(err, "cannot configure container:")
|
||||
if err := z.Serve(); err != nil {
|
||||
hlog.PrintBaseError(err, "cannot configure container:")
|
||||
}
|
||||
|
||||
if err := seccomp.Load(seccomp.PresetCommon); err != nil {
|
||||
if err := seccomp.Load(
|
||||
seccomp.Preset(seccomp.PresetStrict, seccomp.AllowMultiarch),
|
||||
seccomp.AllowMultiarch,
|
||||
); err != nil {
|
||||
log.Fatalf("cannot load syscall filter: %v", err)
|
||||
}
|
||||
|
||||
if err := container.Wait(); err != nil {
|
||||
if err := z.Wait(); err != nil {
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
@@ -3,7 +3,7 @@ package setuid
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
. "git.gensokyo.uk/security/fortify/internal/app"
|
||||
. "hakurei.app/cmd/hakurei/internal/app"
|
||||
)
|
||||
|
||||
func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }
|
||||
@@ -13,9 +13,9 @@ import (
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal/app"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
// fine-grained locking and access
|
||||
@@ -86,17 +86,17 @@ func (s *multiStore) List() ([]int, error) {
|
||||
for _, e := range entries {
|
||||
// skip non-directories
|
||||
if !e.IsDir() {
|
||||
fmsg.Verbosef("skipped non-directory entry %q", e.Name())
|
||||
hlog.Verbosef("skipped non-directory entry %q", e.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
// skip non-numerical names
|
||||
if v, err := strconv.Atoi(e.Name()); err != nil {
|
||||
fmsg.Verbosef("skipped non-aid entry %q", e.Name())
|
||||
hlog.Verbosef("skipped non-aid entry %q", e.Name())
|
||||
continue
|
||||
} else {
|
||||
if v < 0 || v > 9999 {
|
||||
fmsg.Verbosef("skipped out of bounds entry %q", e.Name())
|
||||
hlog.Verbosef("skipped out of bounds entry %q", e.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ func (b *multiBackend) load(decode bool) (Entries, error) {
|
||||
}
|
||||
|
||||
// state file consists of an eight byte header, followed by concatenated gobs
|
||||
// of [fst.Config] and [State], if [State.Config] is not nil or offset < 0,
|
||||
// of [hst.Config] and [State], if [State.Config] is not nil or offset < 0,
|
||||
// the first gob is skipped
|
||||
func (b *multiBackend) decodeState(r io.ReadSeeker, state *State) error {
|
||||
offset := make([]byte, 8)
|
||||
@@ -269,7 +269,7 @@ func (b *multiBackend) decodeState(r io.ReadSeeker, state *State) error {
|
||||
return ErrNoConfig
|
||||
}
|
||||
|
||||
state.Config = new(fst.Config)
|
||||
state.Config = new(hst.Config)
|
||||
if _, err := r.Seek(8, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
9
cmd/hakurei/internal/state/multi_test.go
Normal file
9
cmd/hakurei/internal/state/multi_test.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package state_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/cmd/hakurei/internal/state"
|
||||
)
|
||||
|
||||
func TestMulti(t *testing.T) { testStore(t, state.NewMulti(t.TempDir())) }
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal/app"
|
||||
"hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/hst"
|
||||
)
|
||||
|
||||
var ErrNoConfig = errors.New("state does not contain config")
|
||||
@@ -35,14 +35,14 @@ type Cursor interface {
|
||||
Len() (int, error)
|
||||
}
|
||||
|
||||
// State is a fortify process's state
|
||||
// State is an instance state
|
||||
type State struct {
|
||||
// fortify instance id
|
||||
// hakurei instance id
|
||||
ID app.ID `json:"instance"`
|
||||
// child process PID value
|
||||
PID int `json:"pid"`
|
||||
// sealed app configuration
|
||||
Config *fst.Config `json:"config"`
|
||||
Config *hst.Config `json:"config"`
|
||||
|
||||
// process start time
|
||||
Time time.Time `json:"time"`
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal/app"
|
||||
"git.gensokyo.uk/security/fortify/internal/state"
|
||||
"hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/cmd/hakurei/internal/state"
|
||||
"hakurei.app/hst"
|
||||
)
|
||||
|
||||
func testStore(t *testing.T, s state.Store) {
|
||||
@@ -63,7 +63,7 @@ func testStore(t *testing.T, s state.Store) {
|
||||
&tc[i].state.ID)
|
||||
} else {
|
||||
got.Time = tc[i].state.Time
|
||||
tc[i].state.Config = fst.Template()
|
||||
tc[i].state.Config = hst.Template()
|
||||
if !reflect.DeepEqual(got, &tc[i].state) {
|
||||
t.Fatalf("Load: entry %s got %#v, want %#v",
|
||||
&tc[i].state.ID, got, &tc[i].state)
|
||||
@@ -137,7 +137,7 @@ func makeState(t *testing.T, s *state.State, ct io.Writer) {
|
||||
if err := app.NewAppID(&s.ID); err != nil {
|
||||
t.Fatalf("cannot create dummy state: %v", err)
|
||||
}
|
||||
if err := gob.NewEncoder(ct).Encode(fst.Template()); err != nil {
|
||||
if err := gob.NewEncoder(ct).Encode(hst.Template()); err != nil {
|
||||
t.Fatalf("cannot encode dummy config: %v", err)
|
||||
}
|
||||
s.PID = rand.Int()
|
||||
51
cmd/hakurei/main.go
Normal file
51
cmd/hakurei/main.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package main
|
||||
|
||||
// this works around go:embed '..' limitation
|
||||
//go:generate cp ../../LICENSE .
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/internal/sys"
|
||||
)
|
||||
|
||||
var (
|
||||
errSuccess = errors.New("success")
|
||||
|
||||
//go:embed LICENSE
|
||||
license string
|
||||
)
|
||||
|
||||
func init() { hlog.Prepare("hakurei") }
|
||||
|
||||
var std sys.State = new(sys.Std)
|
||||
|
||||
func main() {
|
||||
// early init path, skips root check and duplicate PR_SET_DUMPABLE
|
||||
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
|
||||
|
||||
if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil {
|
||||
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||
// not fatal: this program runs as the privileged user
|
||||
}
|
||||
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("this program must not run as root")
|
||||
}
|
||||
|
||||
buildCommand(os.Stderr).MustParse(os.Args[1:], func(err error) {
|
||||
hlog.Verbosef("command returned %v", err)
|
||||
if errors.Is(err, errSuccess) {
|
||||
hlog.BeforeExit()
|
||||
os.Exit(0)
|
||||
}
|
||||
// this catches faulty command handlers that fail to return before this point
|
||||
})
|
||||
log.Fatal("unreachable")
|
||||
}
|
||||
@@ -10,19 +10,19 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"git.gensokyo.uk/security/fortify/internal/state"
|
||||
"hakurei.app/cmd/hakurei/internal/state"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
func tryPath(name string) (config *fst.Config) {
|
||||
func tryPath(name string) (config *hst.Config) {
|
||||
var r io.Reader
|
||||
config = new(fst.Config)
|
||||
config = new(hst.Config)
|
||||
|
||||
if name != "-" {
|
||||
r = tryFd(name)
|
||||
if r == nil {
|
||||
fmsg.Verbose("load configuration from file")
|
||||
hlog.Verbose("load configuration from file")
|
||||
|
||||
if f, err := os.Open(name); err != nil {
|
||||
log.Fatalf("cannot access configuration file %q: %s", name, err)
|
||||
@@ -51,11 +51,11 @@ func tryPath(name string) (config *fst.Config) {
|
||||
func tryFd(name string) io.ReadCloser {
|
||||
if v, err := strconv.Atoi(name); err != nil {
|
||||
if !errors.Is(err, strconv.ErrSyntax) {
|
||||
fmsg.Verbosef("name cannot be interpreted as int64: %v", err)
|
||||
hlog.Verbosef("name cannot be interpreted as int64: %v", err)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
fmsg.Verbosef("trying config stream from %d", v)
|
||||
hlog.Verbosef("trying config stream from %d", v)
|
||||
fd := uintptr(v)
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
|
||||
if errors.Is(errno, syscall.EBADF) {
|
||||
@@ -67,7 +67,7 @@ func tryFd(name string) io.ReadCloser {
|
||||
}
|
||||
}
|
||||
|
||||
func tryShort(name string) (config *fst.Config, entry *state.State) {
|
||||
func tryShort(name string) (config *hst.Config, entry *state.State) {
|
||||
likePrefix := false
|
||||
if len(name) <= 32 {
|
||||
likePrefix = true
|
||||
@@ -85,7 +85,7 @@ func tryShort(name string) (config *fst.Config, entry *state.State) {
|
||||
|
||||
// try to match from state store
|
||||
if likePrefix && len(name) >= 8 {
|
||||
fmsg.Verbose("argument looks like prefix")
|
||||
hlog.Verbose("argument looks like prefix")
|
||||
|
||||
s := state.NewMulti(std.Paths().RunDirPath)
|
||||
if entries, err := state.Join(s); err != nil {
|
||||
@@ -101,7 +101,7 @@ func tryShort(name string) (config *fst.Config, entry *state.State) {
|
||||
break
|
||||
}
|
||||
|
||||
fmsg.Verbosef("instance %s skipped", v)
|
||||
hlog.Verbosef("instance %s skipped", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,21 +12,21 @@ import (
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/dbus"
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"git.gensokyo.uk/security/fortify/internal/state"
|
||||
"hakurei.app/cmd/hakurei/internal/state"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
func printShowSystem(output io.Writer, short, flagJSON bool) {
|
||||
t := newPrinter(output)
|
||||
defer t.MustFlush()
|
||||
|
||||
info := new(fst.Info)
|
||||
info := new(hst.Info)
|
||||
|
||||
// get fid by querying uid of aid 0
|
||||
if uid, err := std.Uid(0); err != nil {
|
||||
fmsg.PrintBaseError(err, "cannot obtain uid from fsu:")
|
||||
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
info.User = (uid / 10000) - 100
|
||||
@@ -42,7 +42,7 @@ func printShowSystem(output io.Writer, short, flagJSON bool) {
|
||||
|
||||
func printShowInstance(
|
||||
output io.Writer, now time.Time,
|
||||
instance *state.State, config *fst.Config,
|
||||
instance *state.State, config *hst.Config,
|
||||
short, flagJSON bool) {
|
||||
if flagJSON {
|
||||
if instance != nil {
|
||||
@@ -69,9 +69,9 @@ func printShowInstance(
|
||||
|
||||
t.Printf("App\n")
|
||||
if config.ID != "" {
|
||||
t.Printf(" ID:\t%d (%s)\n", config.Identity, config.ID)
|
||||
t.Printf(" Identity:\t%d (%s)\n", config.Identity, config.ID)
|
||||
} else {
|
||||
t.Printf(" ID:\t%d\n", config.Identity)
|
||||
t.Printf(" Identity:\t%d\n", config.Identity)
|
||||
}
|
||||
t.Printf(" Enablements:\t%s\n", config.Enablements.String())
|
||||
if len(config.Groups) > 0 {
|
||||
@@ -264,7 +264,7 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo
|
||||
as = strconv.Itoa(e.Config.Identity)
|
||||
id := e.Config.ID
|
||||
if id == "" {
|
||||
id = "uk.gensokyo.fortify." + e.s[:8]
|
||||
id = "app.hakurei." + e.s[:8]
|
||||
}
|
||||
as += " (" + id + ")"
|
||||
}
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/dbus"
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal/app"
|
||||
"git.gensokyo.uk/security/fortify/internal/state"
|
||||
"hakurei.app/cmd/hakurei/internal/app"
|
||||
"hakurei.app/cmd/hakurei/internal/state"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -21,7 +21,7 @@ var (
|
||||
testState = &state.State{
|
||||
ID: testID,
|
||||
PID: 0xDEADBEEF,
|
||||
Config: fst.Template(),
|
||||
Config: hst.Template(),
|
||||
Time: testAppTime,
|
||||
}
|
||||
testTime = time.Unix(3752, 1).UTC()
|
||||
@@ -32,15 +32,15 @@ func Test_printShowInstance(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
instance *state.State
|
||||
config *fst.Config
|
||||
config *hst.Config
|
||||
short, json bool
|
||||
want string
|
||||
}{
|
||||
{"config", nil, fst.Template(), false, false, `App
|
||||
ID: 9 (org.chromium.Chromium)
|
||||
{"config", nil, hst.Template(), false, false, `App
|
||||
Identity: 9 (org.chromium.Chromium)
|
||||
Enablements: wayland, dbus, pulseaudio
|
||||
Groups: video, dialout, plugdev
|
||||
Data: /var/lib/fortify/u0/org.chromium.Chromium
|
||||
Data: /var/lib/hakurei/u0/org.chromium.Chromium
|
||||
Hostname: localhost
|
||||
Flags: userns devel net device tty mapuid autoetc
|
||||
Etc: /etc
|
||||
@@ -53,12 +53,12 @@ Filesystem
|
||||
+/run/current-system
|
||||
+/run/opengl-driver
|
||||
+/var/db/nix-channels
|
||||
w*/var/lib/fortify/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
|
||||
w*/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
|
||||
d+/dev/dri
|
||||
|
||||
Extra ACL
|
||||
--x+:/var/lib/fortify/u0
|
||||
rwx:/var/lib/fortify/u0/org.chromium.Chromium
|
||||
--x+:/var/lib/hakurei/u0
|
||||
rwx:/var/lib/hakurei/u0/org.chromium.Chromium
|
||||
|
||||
Session bus
|
||||
Filter: true
|
||||
@@ -72,23 +72,23 @@ System bus
|
||||
Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
|
||||
|
||||
`},
|
||||
{"config pd", nil, new(fst.Config), false, false, `Warning: this configuration uses permissive defaults!
|
||||
{"config pd", nil, new(hst.Config), false, false, `Warning: this configuration uses permissive defaults!
|
||||
|
||||
App
|
||||
ID: 0
|
||||
Identity: 0
|
||||
Enablements: (no enablements)
|
||||
|
||||
`},
|
||||
{"config flag none", nil, &fst.Config{Container: new(fst.ContainerConfig)}, false, false, `App
|
||||
ID: 0
|
||||
{"config flag none", nil, &hst.Config{Container: new(hst.ContainerConfig)}, false, false, `App
|
||||
Identity: 0
|
||||
Enablements: (no enablements)
|
||||
Flags: none
|
||||
Etc: /etc
|
||||
Path:
|
||||
|
||||
`},
|
||||
{"config nil entries", nil, &fst.Config{Container: &fst.ContainerConfig{Filesystem: make([]*fst.FilesystemConfig, 1)}, ExtraPerms: make([]*fst.ExtraPermConfig, 1)}, false, false, `App
|
||||
ID: 0
|
||||
{"config nil entries", nil, &hst.Config{Container: &hst.ContainerConfig{Filesystem: make([]*hst.FilesystemConfig, 1)}, ExtraPerms: make([]*hst.ExtraPermConfig, 1)}, false, false, `App
|
||||
Identity: 0
|
||||
Enablements: (no enablements)
|
||||
Flags: none
|
||||
Etc: /etc
|
||||
@@ -99,10 +99,10 @@ Filesystem
|
||||
Extra ACL
|
||||
|
||||
`},
|
||||
{"config pd dbus see", nil, &fst.Config{SessionBus: &dbus.Config{See: []string{"org.example.test"}}}, false, false, `Warning: this configuration uses permissive defaults!
|
||||
{"config pd dbus see", nil, &hst.Config{SessionBus: &dbus.Config{See: []string{"org.example.test"}}}, false, false, `Warning: this configuration uses permissive defaults!
|
||||
|
||||
App
|
||||
ID: 0
|
||||
Identity: 0
|
||||
Enablements: (no enablements)
|
||||
|
||||
Session bus
|
||||
@@ -111,15 +111,15 @@ Session bus
|
||||
|
||||
`},
|
||||
|
||||
{"instance", testState, fst.Template(), false, false, `State
|
||||
{"instance", testState, hst.Template(), false, false, `State
|
||||
Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559)
|
||||
Uptime: 1h2m32s
|
||||
|
||||
App
|
||||
ID: 9 (org.chromium.Chromium)
|
||||
Identity: 9 (org.chromium.Chromium)
|
||||
Enablements: wayland, dbus, pulseaudio
|
||||
Groups: video, dialout, plugdev
|
||||
Data: /var/lib/fortify/u0/org.chromium.Chromium
|
||||
Data: /var/lib/hakurei/u0/org.chromium.Chromium
|
||||
Hostname: localhost
|
||||
Flags: userns devel net device tty mapuid autoetc
|
||||
Etc: /etc
|
||||
@@ -132,12 +132,12 @@ Filesystem
|
||||
+/run/current-system
|
||||
+/run/opengl-driver
|
||||
+/var/db/nix-channels
|
||||
w*/var/lib/fortify/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
|
||||
w*/var/lib/hakurei/u0/org.chromium.Chromium:/data/data/org.chromium.Chromium
|
||||
d+/dev/dri
|
||||
|
||||
Extra ACL
|
||||
--x+:/var/lib/fortify/u0
|
||||
rwx:/var/lib/fortify/u0/org.chromium.Chromium
|
||||
--x+:/var/lib/hakurei/u0
|
||||
rwx:/var/lib/hakurei/u0/org.chromium.Chromium
|
||||
|
||||
Session bus
|
||||
Filter: true
|
||||
@@ -151,14 +151,14 @@ System bus
|
||||
Talk: ["org.bluez" "org.freedesktop.Avahi" "org.freedesktop.UPower"]
|
||||
|
||||
`},
|
||||
{"instance pd", testState, new(fst.Config), false, false, `Warning: this configuration uses permissive defaults!
|
||||
{"instance pd", testState, new(hst.Config), false, false, `Warning: this configuration uses permissive defaults!
|
||||
|
||||
State
|
||||
Instance: 8e2c76b066dabe574cf073bdb46eb5c1 (3735928559)
|
||||
Uptime: 1h2m32s
|
||||
|
||||
App
|
||||
ID: 0
|
||||
Identity: 0
|
||||
Enablements: (no enablements)
|
||||
|
||||
`},
|
||||
@@ -234,16 +234,16 @@ App
|
||||
},
|
||||
"username": "chronos",
|
||||
"shell": "/run/current-system/sw/bin/zsh",
|
||||
"data": "/var/lib/fortify/u0/org.chromium.Chromium",
|
||||
"data": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"dir": "/data/data/org.chromium.Chromium",
|
||||
"extra_perms": [
|
||||
{
|
||||
"ensure": true,
|
||||
"path": "/var/lib/fortify/u0",
|
||||
"path": "/var/lib/hakurei/u0",
|
||||
"x": true
|
||||
},
|
||||
{
|
||||
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
|
||||
"path": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"r": true,
|
||||
"w": true,
|
||||
"x": true
|
||||
@@ -257,7 +257,8 @@ App
|
||||
],
|
||||
"container": {
|
||||
"hostname": "localhost",
|
||||
"seccomp": 32,
|
||||
"seccomp_flags": 1,
|
||||
"seccomp_presets": 1,
|
||||
"devel": true,
|
||||
"userns": true,
|
||||
"net": true,
|
||||
@@ -285,7 +286,7 @@ App
|
||||
},
|
||||
{
|
||||
"dst": "/data/data/org.chromium.Chromium",
|
||||
"src": "/var/lib/fortify/u0/org.chromium.Chromium",
|
||||
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"write": true,
|
||||
"require": true
|
||||
},
|
||||
@@ -310,7 +311,7 @@ App
|
||||
"time": "1970-01-01T00:00:00.000000009Z"
|
||||
}
|
||||
`},
|
||||
{"json config", nil, fst.Template(), false, true, `{
|
||||
{"json config", nil, hst.Template(), false, true, `{
|
||||
"id": "org.chromium.Chromium",
|
||||
"path": "/run/current-system/sw/bin/chromium",
|
||||
"args": [
|
||||
@@ -359,16 +360,16 @@ App
|
||||
},
|
||||
"username": "chronos",
|
||||
"shell": "/run/current-system/sw/bin/zsh",
|
||||
"data": "/var/lib/fortify/u0/org.chromium.Chromium",
|
||||
"data": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"dir": "/data/data/org.chromium.Chromium",
|
||||
"extra_perms": [
|
||||
{
|
||||
"ensure": true,
|
||||
"path": "/var/lib/fortify/u0",
|
||||
"path": "/var/lib/hakurei/u0",
|
||||
"x": true
|
||||
},
|
||||
{
|
||||
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
|
||||
"path": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"r": true,
|
||||
"w": true,
|
||||
"x": true
|
||||
@@ -382,7 +383,8 @@ App
|
||||
],
|
||||
"container": {
|
||||
"hostname": "localhost",
|
||||
"seccomp": 32,
|
||||
"seccomp_flags": 1,
|
||||
"seccomp_presets": 1,
|
||||
"devel": true,
|
||||
"userns": true,
|
||||
"net": true,
|
||||
@@ -410,7 +412,7 @@ App
|
||||
},
|
||||
{
|
||||
"dst": "/data/data/org.chromium.Chromium",
|
||||
"src": "/var/lib/fortify/u0/org.chromium.Chromium",
|
||||
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"write": true,
|
||||
"require": true
|
||||
},
|
||||
@@ -460,8 +462,8 @@ func Test_printPs(t *testing.T) {
|
||||
{"nil instance", state.Entries{testID: nil}, false, false, " Instance PID Application Uptime\n"},
|
||||
{"state corruption", state.Entries{app.ID{}: testState}, false, false, " Instance PID Application Uptime\n"},
|
||||
|
||||
{"valid pd", state.Entries{testID: &state.State{ID: testID, PID: 1 << 8, Config: new(fst.Config), Time: testAppTime}}, false, false, ` Instance PID Application Uptime
|
||||
8e2c76b0 256 0 (uk.gensokyo.fortify.8e2c76b0) 1h2m32s
|
||||
{"valid pd", state.Entries{testID: &state.State{ID: testID, PID: 1 << 8, Config: new(hst.Config), Time: testAppTime}}, false, false, ` Instance PID Application Uptime
|
||||
8e2c76b0 256 0 (app.hakurei.8e2c76b0) 1h2m32s
|
||||
`},
|
||||
|
||||
{"valid", state.Entries{testID: testState}, false, false, ` Instance PID Application Uptime
|
||||
@@ -538,16 +540,16 @@ func Test_printPs(t *testing.T) {
|
||||
},
|
||||
"username": "chronos",
|
||||
"shell": "/run/current-system/sw/bin/zsh",
|
||||
"data": "/var/lib/fortify/u0/org.chromium.Chromium",
|
||||
"data": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"dir": "/data/data/org.chromium.Chromium",
|
||||
"extra_perms": [
|
||||
{
|
||||
"ensure": true,
|
||||
"path": "/var/lib/fortify/u0",
|
||||
"path": "/var/lib/hakurei/u0",
|
||||
"x": true
|
||||
},
|
||||
{
|
||||
"path": "/var/lib/fortify/u0/org.chromium.Chromium",
|
||||
"path": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"r": true,
|
||||
"w": true,
|
||||
"x": true
|
||||
@@ -561,7 +563,8 @@ func Test_printPs(t *testing.T) {
|
||||
],
|
||||
"container": {
|
||||
"hostname": "localhost",
|
||||
"seccomp": 32,
|
||||
"seccomp_flags": 1,
|
||||
"seccomp_presets": 1,
|
||||
"devel": true,
|
||||
"userns": true,
|
||||
"net": true,
|
||||
@@ -589,7 +592,7 @@ func Test_printPs(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"dst": "/data/data/org.chromium.Chromium",
|
||||
"src": "/var/lib/fortify/u0/org.chromium.Chromium",
|
||||
"src": "/var/lib/hakurei/u0/org.chromium.Chromium",
|
||||
"write": true,
|
||||
"require": true
|
||||
},
|
||||
@@ -13,17 +13,17 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
fsuConfFile = "/etc/fsurc"
|
||||
envShim = "FORTIFY_SHIM"
|
||||
envAID = "FORTIFY_APP_ID"
|
||||
envGroups = "FORTIFY_GROUPS"
|
||||
hsuConfFile = "/etc/hsurc"
|
||||
envShim = "HAKUREI_SHIM"
|
||||
envAID = "HAKUREI_APP_ID"
|
||||
envGroups = "HAKUREI_GROUPS"
|
||||
|
||||
PR_SET_NO_NEW_PRIVS = 0x26
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("fsu: ")
|
||||
log.SetPrefix("hsu: ")
|
||||
log.SetOutput(os.Stderr)
|
||||
|
||||
if os.Geteuid() != 0 {
|
||||
@@ -40,9 +40,9 @@ func main() {
|
||||
if p, err := os.Readlink(pexe); err != nil {
|
||||
log.Fatalf("cannot read parent executable path: %v", err)
|
||||
} else if strings.HasSuffix(p, " (deleted)") {
|
||||
log.Fatal("fortify executable has been deleted")
|
||||
} else if p != mustCheckPath(fmain) && p != mustCheckPath(fpkg) {
|
||||
log.Fatal("this program must be started by fortify")
|
||||
log.Fatal("hakurei executable has been deleted")
|
||||
} else if p != mustCheckPath(hmain) {
|
||||
log.Fatal("this program must be started by hakurei")
|
||||
} else {
|
||||
toolPath = p
|
||||
}
|
||||
@@ -52,27 +52,27 @@ func main() {
|
||||
// aid
|
||||
uid := 1000000
|
||||
|
||||
// refuse to run if fsurc is not protected correctly
|
||||
if s, err := os.Stat(fsuConfFile); err != nil {
|
||||
// refuse to run if hsurc is not protected correctly
|
||||
if s, err := os.Stat(hsuConfFile); err != nil {
|
||||
log.Fatal(err)
|
||||
} else if s.Mode().Perm() != 0400 {
|
||||
log.Fatal("bad fsurc perm")
|
||||
log.Fatal("bad hsurc perm")
|
||||
} else if st := s.Sys().(*syscall.Stat_t); st.Uid != 0 || st.Gid != 0 {
|
||||
log.Fatal("fsurc must be owned by uid 0")
|
||||
log.Fatal("hsurc must be owned by uid 0")
|
||||
}
|
||||
|
||||
// authenticate before accepting user input
|
||||
if f, err := os.Open(fsuConfFile); err != nil {
|
||||
if f, err := os.Open(hsuConfFile); err != nil {
|
||||
log.Fatal(err)
|
||||
} else if fid, ok := mustParseConfig(f, puid); !ok {
|
||||
log.Fatalf("uid %d is not in the fsurc file", puid)
|
||||
log.Fatalf("uid %d is not in the hsurc file", puid)
|
||||
} else {
|
||||
uid += fid * 10000
|
||||
}
|
||||
|
||||
// allowed aid range 0 to 9999
|
||||
if as, ok := os.LookupEnv(envAID); !ok {
|
||||
log.Fatal("FORTIFY_APP_ID not set")
|
||||
log.Fatal("HAKUREI_APP_ID not set")
|
||||
} else if aid, err := parseUint32Fast(as); err != nil || aid < 0 || aid > 9999 {
|
||||
log.Fatal("invalid aid")
|
||||
} else {
|
||||
@@ -82,12 +82,12 @@ func main() {
|
||||
// pass through setup fd to shim
|
||||
var shimSetupFd string
|
||||
if s, ok := os.LookupEnv(envShim); !ok {
|
||||
// fortify requests target uid
|
||||
// hakurei requests target uid
|
||||
// print resolved uid and exit
|
||||
fmt.Print(uid)
|
||||
os.Exit(0)
|
||||
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' {
|
||||
log.Fatal("FORTIFY_SHIM holds an invalid value")
|
||||
log.Fatal("HAKUREI_SHIM holds an invalid value")
|
||||
} else {
|
||||
shimSetupFd = s
|
||||
}
|
||||
@@ -124,7 +124,7 @@ func main() {
|
||||
panic("uid out of bounds")
|
||||
}
|
||||
|
||||
// careful! users in the allowlist is effectively allowed to drop groups via fsu
|
||||
// careful! users in the allowlist is effectively allowed to drop groups via hsu
|
||||
|
||||
if err := syscall.Setresgid(uid, uid, uid); err != nil {
|
||||
log.Fatalf("cannot set gid: %v", err)
|
||||
@@ -138,7 +138,7 @@ func main() {
|
||||
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
|
||||
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
|
||||
}
|
||||
if err := syscall.Exec(toolPath, []string{"fortify", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
|
||||
if err := syscall.Exec(toolPath, []string{"hakurei", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
|
||||
log.Fatalf("cannot start shim: %v", err)
|
||||
}
|
||||
|
||||
23
cmd/hsu/package.nix
Normal file
23
cmd/hsu/package.nix
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
lib,
|
||||
buildGoModule,
|
||||
hakurei ? abort "hakurei package required",
|
||||
}:
|
||||
|
||||
buildGoModule {
|
||||
pname = "${hakurei.pname}-hsu";
|
||||
inherit (hakurei) version;
|
||||
|
||||
src = ./.;
|
||||
inherit (hakurei) vendorHash;
|
||||
env.CGO_ENABLED = 0;
|
||||
|
||||
preBuild = ''
|
||||
go mod init hsu >& /dev/null
|
||||
'';
|
||||
|
||||
ldflags = lib.attrsets.foldlAttrs (
|
||||
ldflags: name: value:
|
||||
ldflags ++ [ "-X main.${name}=${value}" ]
|
||||
) [ "-s -w" ] { hmain = "${hakurei}/libexec/hakurei"; };
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func parseConfig(r io.Reader, puid int) (fid int, ok bool, err error) {
|
||||
if ok {
|
||||
// allowed fid range 0 to 99
|
||||
if fid, err = parseUint32Fast(lf[1]); err != nil || fid < 0 || fid > 99 {
|
||||
return -1, false, fmt.Errorf("invalid fortify uid on line %d", line)
|
||||
return -1, false, fmt.Errorf("invalid identity on line %d", line)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func Test_parseConfig(t *testing.T) {
|
||||
{"empty", 0, -1, "", ``},
|
||||
{"invalid field", 0, -1, "invalid entry on line 1", `9`},
|
||||
{"invalid puid", 0, -1, "invalid parent uid on line 1", `f 9`},
|
||||
{"invalid fid", 1000, -1, "invalid fortify uid on line 1", `1000 f`},
|
||||
{"invalid fid", 1000, -1, "invalid identity on line 1", `1000 f`},
|
||||
{"match", 1000, 0, "", `1000 0`},
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@ import (
|
||||
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
||||
|
||||
var (
|
||||
fmain = compPoison
|
||||
fpkg = compPoison
|
||||
hmain = compPoison
|
||||
)
|
||||
|
||||
func mustCheckPath(p string) string {
|
||||
@@ -6,46 +6,46 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/dbus"
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||
"git.gensokyo.uk/security/fortify/system"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
type appInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
|
||||
// passed through to [fst.Config]
|
||||
// passed through to [hst.Config]
|
||||
ID string `json:"id"`
|
||||
// passed through to [fst.Config]
|
||||
// passed through to [hst.Config]
|
||||
Identity int `json:"identity"`
|
||||
// passed through to [fst.Config]
|
||||
// passed through to [hst.Config]
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
// passed through to [hst.Config]
|
||||
Devel bool `json:"devel,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
// passed through to [hst.Config]
|
||||
Userns bool `json:"userns,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
// passed through to [hst.Config]
|
||||
Net bool `json:"net,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
// passed through to [hst.Config]
|
||||
Device bool `json:"dev,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
// passed through to [hst.Config]
|
||||
Tty bool `json:"tty,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
// passed through to [hst.Config]
|
||||
MapRealUID bool `json:"map_real_uid,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
// passed through to [hst.Config]
|
||||
DirectWayland bool `json:"direct_wayland,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
// passed through to [hst.Config]
|
||||
SystemBus *dbus.Config `json:"system_bus,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
// passed through to [hst.Config]
|
||||
SessionBus *dbus.Config `json:"session_bus,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
// passed through to [hst.Config]
|
||||
Enablements system.Enablement `json:"enablements"`
|
||||
|
||||
// passed through to [fst.Config]
|
||||
// passed through to [hst.Config]
|
||||
Multiarch bool `json:"multiarch,omitempty"`
|
||||
// passed through to [fst.Config]
|
||||
// passed through to [hst.Config]
|
||||
Bluetooth bool `json:"bluetooth,omitempty"`
|
||||
|
||||
// allow gpu access within sandbox
|
||||
@@ -62,8 +62,8 @@ type appInfo struct {
|
||||
ActivationPackage string `json:"activation_package"`
|
||||
}
|
||||
|
||||
func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool) *fst.Config {
|
||||
config := &fst.Config{
|
||||
func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool) *hst.Config {
|
||||
config := &hst.Config{
|
||||
ID: app.ID,
|
||||
|
||||
Path: argv[0],
|
||||
@@ -75,7 +75,7 @@ func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool
|
||||
SessionBus: app.SessionBus,
|
||||
DirectWayland: app.DirectWayland,
|
||||
|
||||
Username: "fortify",
|
||||
Username: "hakurei",
|
||||
Shell: shellPath,
|
||||
Data: pathSet.homeDir,
|
||||
Dir: path.Join("/data/data", app.ID),
|
||||
@@ -83,7 +83,7 @@ func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool
|
||||
Identity: app.Identity,
|
||||
Groups: app.Groups,
|
||||
|
||||
Container: &fst.ContainerConfig{
|
||||
Container: &hst.ContainerConfig{
|
||||
Hostname: formatHostname(app.Name),
|
||||
Devel: app.Devel,
|
||||
Userns: app.Userns,
|
||||
@@ -91,9 +91,9 @@ func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool
|
||||
Device: app.Device,
|
||||
Tty: app.Tty || flagDropShell,
|
||||
MapRealUID: app.MapRealUID,
|
||||
Filesystem: []*fst.FilesystemConfig{
|
||||
Filesystem: []*hst.FilesystemConfig{
|
||||
{Src: path.Join(pathSet.nixPath, "store"), Dst: "/nix/store", Must: true},
|
||||
{Src: pathSet.metaPath, Dst: path.Join(fst.Tmp, "app"), Must: true},
|
||||
{Src: pathSet.metaPath, Dst: path.Join(hst.Tmp, "app"), Must: true},
|
||||
{Src: "/etc/resolv.conf"},
|
||||
{Src: "/sys/block"},
|
||||
{Src: "/sys/bus"},
|
||||
@@ -109,16 +109,16 @@ func (app *appInfo) toFst(pathSet *appPathSet, argv []string, flagDropShell bool
|
||||
Etc: path.Join(pathSet.cacheDir, "etc"),
|
||||
AutoEtc: true,
|
||||
},
|
||||
ExtraPerms: []*fst.ExtraPermConfig{
|
||||
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
|
||||
config.Container.SeccompFlags |= seccomp.AllowMultiarch
|
||||
}
|
||||
if app.Bluetooth {
|
||||
config.Container.Seccomp |= seccomp.FilterBluetooth
|
||||
config.Container.SeccompFlags |= seccomp.AllowBluetooth
|
||||
}
|
||||
return config
|
||||
}
|
||||
@@ -147,7 +147,7 @@ func loadAppInfo(name string, beforeFail func()) *appInfo {
|
||||
func formatHostname(name string) string {
|
||||
if h, err := os.Hostname(); err != nil {
|
||||
log.Printf("cannot get hostname: %v", err)
|
||||
return "fortify-" + name
|
||||
return "hakurei-" + name
|
||||
} else {
|
||||
return h + "-" + name
|
||||
}
|
||||
@@ -57,7 +57,7 @@ let
|
||||
modules = modules ++ [
|
||||
{
|
||||
home = {
|
||||
username = "fortify";
|
||||
username = "hakurei";
|
||||
homeDirectory = "/data/data/${id}";
|
||||
stateVersion = "22.11";
|
||||
};
|
||||
@@ -65,7 +65,7 @@ let
|
||||
];
|
||||
};
|
||||
|
||||
launcher = writeScript "fortify-${pname}" ''
|
||||
launcher = writeScript "hakurei-${pname}" ''
|
||||
#!${runtimeShell} -el
|
||||
${script}
|
||||
'';
|
||||
@@ -10,39 +10,26 @@ import (
|
||||
"path"
|
||||
"syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/command"
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
"git.gensokyo.uk/security/fortify/internal/app/instance"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"git.gensokyo.uk/security/fortify/internal/sys"
|
||||
"git.gensokyo.uk/security/fortify/sandbox"
|
||||
"hakurei.app/command"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
const shellPath = "/run/current-system/sw/bin/bash"
|
||||
|
||||
var (
|
||||
errSuccess = errors.New("success")
|
||||
|
||||
std sys.State = new(sys.Std)
|
||||
)
|
||||
|
||||
func init() {
|
||||
fmsg.Prepare("fpkg")
|
||||
hlog.Prepare("planterette")
|
||||
if err := os.Setenv("SHELL", shellPath); err != nil {
|
||||
log.Fatalf("cannot set $SHELL: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// early init path, skips root check and duplicate PR_SET_DUMPABLE
|
||||
sandbox.TryArgv0(fmsg.Output{}, fmsg.Prepare, internal.InstallFmsg)
|
||||
|
||||
if err := sandbox.SetDumpable(sandbox.SUID_DUMP_DISABLE); err != nil {
|
||||
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||
// not fatal: this program runs as the privileged user
|
||||
}
|
||||
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("this program must not run as root")
|
||||
}
|
||||
@@ -55,14 +42,9 @@ func main() {
|
||||
flagVerbose bool
|
||||
flagDropShell bool
|
||||
)
|
||||
c := command.New(os.Stderr, log.Printf, "fpkg", func([]string) error {
|
||||
internal.InstallFmsg(flagVerbose)
|
||||
return nil
|
||||
}).
|
||||
c := command.New(os.Stderr, log.Printf, "planterette", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
|
||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
|
||||
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next fortify action")
|
||||
|
||||
c.Command("shim", command.UsageInternal, func([]string) error { instance.ShimMain(); return errSuccess })
|
||||
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action")
|
||||
|
||||
{
|
||||
var (
|
||||
@@ -84,7 +66,7 @@ func main() {
|
||||
}
|
||||
|
||||
/*
|
||||
Look up paths to programs started by fpkg.
|
||||
Look up paths to programs started by planterette.
|
||||
This is done here to ease error handling as cleanup is not yet required.
|
||||
*/
|
||||
|
||||
@@ -100,7 +82,7 @@ func main() {
|
||||
*/
|
||||
|
||||
var workDir string
|
||||
if p, err := os.MkdirTemp("", "fpkg.*"); err != nil {
|
||||
if p, err := os.MkdirTemp("", "planterette.*"); err != nil {
|
||||
log.Printf("cannot create temporary directory: %v", err)
|
||||
return err
|
||||
} else {
|
||||
@@ -166,10 +148,10 @@ func main() {
|
||||
}
|
||||
|
||||
// sec: should compare version string
|
||||
fmsg.Verbosef("installing application %q version %q over local %q",
|
||||
hlog.Verbosef("installing application %q version %q over local %q",
|
||||
bundle.ID, bundle.Version, a.Version)
|
||||
} else {
|
||||
fmsg.Verbosef("application %q clean installation", bundle.ID)
|
||||
hlog.Verbosef("application %q clean installation", bundle.ID)
|
||||
// sec: should install credentials
|
||||
}
|
||||
|
||||
@@ -179,7 +161,7 @@ func main() {
|
||||
|
||||
withCacheDir(ctx, "install", []string{
|
||||
// export inner bundle path in the environment
|
||||
"export BUNDLE=" + fst.Tmp + "/bundle",
|
||||
"export BUNDLE=" + hst.Tmp + "/bundle",
|
||||
// replace inner /etc
|
||||
"mkdir -p etc",
|
||||
"chmod -R +w etc",
|
||||
@@ -218,7 +200,7 @@ func main() {
|
||||
"rm -rf .local/state/{nix,home-manager}",
|
||||
// run activation script
|
||||
bundle.ActivationPackage + "/activate",
|
||||
}, false, func(config *fst.Config) *fst.Config { return config },
|
||||
}, false, func(config *hst.Config) *hst.Config { return config },
|
||||
bundle, pathSet, flagDropShellActivate, cleanup)
|
||||
|
||||
/*
|
||||
@@ -291,8 +273,8 @@ func main() {
|
||||
"--out-link /nix/.nixGL/auto/vulkan " +
|
||||
"--override-input nixpkgs path:/etc/nixpkgs " +
|
||||
"path:" + a.NixGL + "#nixVulkanNvidia",
|
||||
}, true, func(config *fst.Config) *fst.Config {
|
||||
config.Container.Filesystem = append(config.Container.Filesystem, []*fst.FilesystemConfig{
|
||||
}, 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"},
|
||||
@@ -325,7 +307,7 @@ func main() {
|
||||
|
||||
if a.GPU {
|
||||
config.Container.Filesystem = append(config.Container.Filesystem,
|
||||
&fst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(fst.Tmp, "nixGL")})
|
||||
&hst.FilesystemConfig{Src: path.Join(pathSet.nixPath, ".nixGL"), Dst: path.Join(hst.Tmp, "nixGL")})
|
||||
appendGPUFilesystem(config)
|
||||
}
|
||||
|
||||
@@ -341,9 +323,9 @@ func main() {
|
||||
}
|
||||
|
||||
c.MustParse(os.Args[1:], func(err error) {
|
||||
fmsg.Verbosef("command returned %v", err)
|
||||
hlog.Verbosef("command returned %v", err)
|
||||
if errors.Is(err, errSuccess) {
|
||||
fmsg.BeforeExit()
|
||||
hlog.BeforeExit()
|
||||
os.Exit(0)
|
||||
}
|
||||
})
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -18,10 +18,10 @@ var (
|
||||
|
||||
func init() {
|
||||
// dataHome
|
||||
if p, ok := os.LookupEnv("FORTIFY_DATA_HOME"); ok {
|
||||
if p, ok := os.LookupEnv("HAKUREI_DATA_HOME"); ok {
|
||||
dataHome = p
|
||||
} else {
|
||||
dataHome = "/var/lib/fortify/" + strconv.Itoa(os.Getuid())
|
||||
dataHome = "/var/lib/hakurei/" + strconv.Itoa(os.Getuid())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func lookPath(file string) string {
|
||||
var beforeRunFail = new(atomic.Pointer[func()])
|
||||
|
||||
func mustRun(name string, arg ...string) {
|
||||
fmsg.Verbosef("spawning process: %q %q", name, arg)
|
||||
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 {
|
||||
@@ -71,8 +71,8 @@ func pathSetByApp(id string) *appPathSet {
|
||||
return pathSet
|
||||
}
|
||||
|
||||
func appendGPUFilesystem(config *fst.Config) {
|
||||
config.Container.Filesystem = append(config.Container.Filesystem, []*fst.FilesystemConfig{
|
||||
func appendGPUFilesystem(config *hst.Config) {
|
||||
config.Container.Filesystem = append(config.Container.Filesystem, []*hst.FilesystemConfig{
|
||||
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
|
||||
{Src: "/dev/dri", Device: true},
|
||||
// mali
|
||||
60
cmd/planterette/proc.go
Normal file
60
cmd/planterette/proc.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
var hakureiPath = internal.MustHakureiPath()
|
||||
|
||||
func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) {
|
||||
var (
|
||||
cmd *exec.Cmd
|
||||
st io.WriteCloser
|
||||
)
|
||||
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot pipe: %v", err)
|
||||
} else {
|
||||
if hlog.Load() {
|
||||
cmd = exec.CommandContext(ctx, hakureiPath, "-v", "app", "3")
|
||||
} else {
|
||||
cmd = exec.CommandContext(ctx, hakureiPath, "app", "3")
|
||||
}
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
cmd.ExtraFiles = []*os.File{r}
|
||||
st = w
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := json.NewEncoder(st).Encode(config); err != nil {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot send configuration: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot start hakurei: %v", err)
|
||||
}
|
||||
if err := cmd.Wait(); err != nil {
|
||||
var exitError *exec.ExitError
|
||||
if errors.As(err, &exitError) {
|
||||
beforeFail()
|
||||
internal.Exit(exitError.ExitCode())
|
||||
} else {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot wait: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,11 +50,13 @@
|
||||
];
|
||||
};
|
||||
|
||||
environment.fortify = {
|
||||
environment.hakurei = {
|
||||
enable = true;
|
||||
stateDir = "/var/lib/fortify";
|
||||
stateDir = "/var/lib/hakurei";
|
||||
users.alice = 0;
|
||||
|
||||
home-manager = _: _: { home.stateVersion = "23.05"; };
|
||||
extraHomeConfig = {
|
||||
home.stateVersion = "23.05";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -9,7 +9,7 @@ let
|
||||
buildPackage = self.buildPackage.${system};
|
||||
in
|
||||
nixosTest {
|
||||
name = "fpkg";
|
||||
name = "planterette";
|
||||
nodes.machine = {
|
||||
environment.etc = {
|
||||
"foot.pkg".source = callPackage ./foot.nix { inherit buildPackage; };
|
||||
@@ -18,7 +18,7 @@ nixosTest {
|
||||
imports = [
|
||||
./configuration.nix
|
||||
|
||||
self.nixosModules.fortify
|
||||
self.nixosModules.hakurei
|
||||
self.inputs.home-manager.nixosModules.home-manager
|
||||
];
|
||||
};
|
||||
@@ -47,22 +47,22 @@ def wait_for_window(pattern):
|
||||
|
||||
|
||||
def collect_state_ui(name):
|
||||
swaymsg(f"exec fortify ps > '/tmp/{name}.ps'")
|
||||
swaymsg(f"exec hakurei ps > '/tmp/{name}.ps'")
|
||||
machine.copy_from_vm(f"/tmp/{name}.ps", "")
|
||||
swaymsg(f"exec fortify --json ps > '/tmp/{name}.json'")
|
||||
swaymsg(f"exec hakurei --json ps > '/tmp/{name}.json'")
|
||||
machine.copy_from_vm(f"/tmp/{name}.json", "")
|
||||
machine.screenshot(name)
|
||||
|
||||
|
||||
def check_state(name, enablements):
|
||||
instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 fortify --json ps"))
|
||||
instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei --json ps"))
|
||||
if len(instances) != 1:
|
||||
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"fortify-{name}-" not in (config['args'][0]):
|
||||
if len(config['args']) != 1 or not (config['args'][0].startswith("/nix/store/")) or f"hakurei-{name}-" not in (config['args'][0]):
|
||||
raise Exception(f"unexpected args {instance['config']['args']}")
|
||||
|
||||
if config['enablements'] != enablements:
|
||||
@@ -72,25 +72,25 @@ def check_state(name, enablements):
|
||||
start_all()
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
# To check fortify's version:
|
||||
print(machine.succeed("sudo -u alice -i fortify version"))
|
||||
# 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 fpkg directory:
|
||||
machine.succeed("install -dm 0700 -o alice -g users /var/lib/fortify/1000")
|
||||
# Prepare planterette directory:
|
||||
machine.succeed("install -dm 0700 -o alice -g users /var/lib/hakurei/1000")
|
||||
|
||||
# Install fpkg app:
|
||||
swaymsg("exec fpkg -v install /etc/foot.pkg && touch /tmp/fpkg-install-done")
|
||||
machine.wait_for_file("/tmp/fpkg-install-done")
|
||||
# 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 fpkg -v start org.codeberg.dnkl.foot")
|
||||
wait_for_window("fortify@machine-foot")
|
||||
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/fortify.1000/tmpdir/2/success-client")
|
||||
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:
|
||||
@@ -104,5 +104,5 @@ machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/
|
||||
swaymsg("exit", succeed=False)
|
||||
machine.wait_for_file("/tmp/sway-exit-ok")
|
||||
|
||||
# Print fortify runDir contents:
|
||||
print(machine.succeed("find /run/user/1000/fortify"))
|
||||
# Print hakurei runDir contents:
|
||||
print(machine.succeed("find /run/user/1000/hakurei"))
|
||||
@@ -5,17 +5,17 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
)
|
||||
|
||||
func withNixDaemon(
|
||||
ctx context.Context,
|
||||
action string, command []string, net bool, updateConfig func(config *fst.Config) *fst.Config,
|
||||
action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config,
|
||||
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
|
||||
) {
|
||||
mustRunAppDropShell(ctx, updateConfig(&fst.Config{
|
||||
mustRunAppDropShell(ctx, updateConfig(&hst.Config{
|
||||
ID: app.ID,
|
||||
|
||||
Path: shellPath,
|
||||
@@ -31,24 +31,24 @@ func withNixDaemon(
|
||||
" && pkill nix-daemon",
|
||||
},
|
||||
|
||||
Username: "fortify",
|
||||
Username: "hakurei",
|
||||
Shell: shellPath,
|
||||
Data: pathSet.homeDir,
|
||||
Dir: path.Join("/data/data", app.ID),
|
||||
ExtraPerms: []*fst.ExtraPermConfig{
|
||||
ExtraPerms: []*hst.ExtraPermConfig{
|
||||
{Path: dataHome, Execute: true},
|
||||
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
|
||||
},
|
||||
|
||||
Identity: app.Identity,
|
||||
|
||||
Container: &fst.ContainerConfig{
|
||||
Hostname: formatHostname(app.Name) + "-" + action,
|
||||
Userns: true, // nix sandbox requires userns
|
||||
Net: net,
|
||||
Seccomp: seccomp.FilterMultiarch,
|
||||
Tty: dropShell,
|
||||
Filesystem: []*fst.FilesystemConfig{
|
||||
Container: &hst.ContainerConfig{
|
||||
Hostname: formatHostname(app.Name) + "-" + action,
|
||||
Userns: true, // nix sandbox requires userns
|
||||
Net: net,
|
||||
SeccompFlags: seccomp.AllowMultiarch,
|
||||
Tty: dropShell,
|
||||
Filesystem: []*hst.FilesystemConfig{
|
||||
{Src: pathSet.nixPath, Dst: "/nix", Write: true, Must: true},
|
||||
},
|
||||
Link: [][2]string{
|
||||
@@ -66,7 +66,7 @@ func withCacheDir(
|
||||
ctx context.Context,
|
||||
action string, command []string, workDir string,
|
||||
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
|
||||
mustRunAppDropShell(ctx, &fst.Config{
|
||||
mustRunAppDropShell(ctx, &hst.Config{
|
||||
ID: app.ID,
|
||||
|
||||
Path: shellPath,
|
||||
@@ -76,7 +76,7 @@ func withCacheDir(
|
||||
Shell: shellPath,
|
||||
Data: pathSet.cacheDir, // this also ensures cacheDir via shim
|
||||
Dir: path.Join("/data/data", app.ID, "cache"),
|
||||
ExtraPerms: []*fst.ExtraPermConfig{
|
||||
ExtraPerms: []*hst.ExtraPermConfig{
|
||||
{Path: dataHome, Execute: true},
|
||||
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
|
||||
{Path: workDir, Execute: true},
|
||||
@@ -84,13 +84,13 @@ func withCacheDir(
|
||||
|
||||
Identity: app.Identity,
|
||||
|
||||
Container: &fst.ContainerConfig{
|
||||
Hostname: formatHostname(app.Name) + "-" + action,
|
||||
Seccomp: seccomp.FilterMultiarch,
|
||||
Tty: dropShell,
|
||||
Filesystem: []*fst.FilesystemConfig{
|
||||
Container: &hst.ContainerConfig{
|
||||
Hostname: formatHostname(app.Name) + "-" + action,
|
||||
SeccompFlags: seccomp.AllowMultiarch,
|
||||
Tty: dropShell,
|
||||
Filesystem: []*hst.FilesystemConfig{
|
||||
{Src: path.Join(workDir, "nix"), Dst: "/nix", Must: true},
|
||||
{Src: workDir, Dst: path.Join(fst.Tmp, "bundle"), Must: true},
|
||||
{Src: workDir, Dst: path.Join(hst.Tmp, "bundle"), Must: true},
|
||||
},
|
||||
Link: [][2]string{
|
||||
{app.CurrentSystem, "/run/current-system"},
|
||||
@@ -103,7 +103,7 @@ func withCacheDir(
|
||||
}, dropShell, beforeFail)
|
||||
}
|
||||
|
||||
func mustRunAppDropShell(ctx context.Context, config *fst.Config, dropShell bool, beforeFail func()) {
|
||||
func mustRunAppDropShell(ctx context.Context, config *hst.Config, dropShell bool, beforeFail func()) {
|
||||
if dropShell {
|
||||
config.Args = []string{shellPath, "-l"}
|
||||
mustRunApp(ctx, config, beforeFail)
|
||||
@@ -3,7 +3,7 @@ package command_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/command"
|
||||
"hakurei.app/command"
|
||||
)
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/command"
|
||||
"hakurei.app/command"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
#compdef fortify
|
||||
|
||||
_fortify_app() {
|
||||
__fortify_files
|
||||
return $?
|
||||
}
|
||||
|
||||
_fortify_run() {
|
||||
_arguments \
|
||||
'--id[App ID, leave empty to disable security context app_id]:id' \
|
||||
'-a[Fortify application ID]: :_numbers' \
|
||||
'-g[Groups inherited by the app process]: :_groups' \
|
||||
'-d[Application home directory]: :_files -/' \
|
||||
'-u[Passwd name within sandbox]: :_users' \
|
||||
'--wayland[Share Wayland socket]' \
|
||||
'-X[Share X11 socket and allow connection]' \
|
||||
'--dbus[Proxy D-Bus connection]' \
|
||||
'--pulse[Share PulseAudio socket and cookie]' \
|
||||
'--dbus-config[Path to D-Bus proxy config file]: :_files -g "*.json"' \
|
||||
'--dbus-system[Path to system D-Bus proxy config file]: :_files -g "*.json"' \
|
||||
'--mpris[Allow owning MPRIS D-Bus path]' \
|
||||
'--dbus-log[Force logging in the D-Bus proxy]'
|
||||
}
|
||||
|
||||
_fortify_ps() {
|
||||
_arguments \
|
||||
'--short[Print instance id]'
|
||||
}
|
||||
|
||||
_fortify_show() {
|
||||
_alternative \
|
||||
'instances:domains:__fortify_instances' \
|
||||
'files:files:__fortify_files'
|
||||
}
|
||||
|
||||
__fortify_files() {
|
||||
_files -g "*.(json|ftfy)"
|
||||
return $?
|
||||
}
|
||||
|
||||
__fortify_instances() {
|
||||
local -a out
|
||||
shift -p
|
||||
out=( ${(f)"$(_call_program commands fortify ps --short 2>&1)"} )
|
||||
if (( $#out == 0 )); then
|
||||
_message "No active instances"
|
||||
else
|
||||
_describe "active instances" out
|
||||
fi
|
||||
return $?
|
||||
}
|
||||
|
||||
(( $+functions[_fortify_commands] )) || _fortify_commands()
|
||||
{
|
||||
local -a _fortify_cmds
|
||||
_fortify_cmds=(
|
||||
"app:Launch app defined by the specified config file"
|
||||
"run:Configure and start a permissive default sandbox"
|
||||
"show:Show the contents of an app configuration"
|
||||
"ps:List active apps and their state"
|
||||
"version:Show fortify version"
|
||||
"license:Show full license text"
|
||||
"template:Produce a config template"
|
||||
"help:Show help message"
|
||||
)
|
||||
if (( CURRENT == 1 )); then
|
||||
_describe -t commands 'action' _fortify_cmds || compadd "$@"
|
||||
else
|
||||
local curcontext="$curcontext"
|
||||
cmd="${${_fortify_cmds[(r)$words[1]:*]%%:*}}"
|
||||
if (( $+functions[_fortify_$cmd] )); then
|
||||
_fortify_$cmd
|
||||
else
|
||||
_message "no more options"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
_arguments -C \
|
||||
'-v[Verbose output]' \
|
||||
'--json[Format output in JSON when applicable]' \
|
||||
'*::fortify command:_fortify_commands'
|
||||
@@ -1,5 +1,5 @@
|
||||
// Package sandbox implements unprivileged Linux container with hardening options useful for creating application sandboxes.
|
||||
package sandbox
|
||||
// Package container implements unprivileged Linux containers with built-in support for syscall filtering.
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -11,38 +11,12 @@ import (
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
"syscall"
|
||||
. "syscall"
|
||||
"time"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||
"hakurei.app/container/seccomp"
|
||||
)
|
||||
|
||||
type HardeningFlags uintptr
|
||||
|
||||
const (
|
||||
FSyscallCompat HardeningFlags = 1 << iota
|
||||
FAllowDevel
|
||||
FAllowUserns
|
||||
FAllowTTY
|
||||
FAllowNet
|
||||
)
|
||||
|
||||
func (flags HardeningFlags) seccomp(opts seccomp.FilterOpts) seccomp.FilterOpts {
|
||||
if flags&FSyscallCompat == 0 {
|
||||
opts |= seccomp.FilterExt
|
||||
}
|
||||
if flags&FAllowDevel == 0 {
|
||||
opts |= seccomp.FilterDenyDevel
|
||||
}
|
||||
if flags&FAllowUserns == 0 {
|
||||
opts |= seccomp.FilterDenyNS
|
||||
}
|
||||
if flags&FAllowTTY == 0 {
|
||||
opts |= seccomp.FilterDenyTTY
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
type (
|
||||
// Container represents a container environment being prepared or run.
|
||||
// None of [Container] methods are safe for concurrent use.
|
||||
@@ -94,15 +68,23 @@ type (
|
||||
Hostname string
|
||||
// Sequential container setup ops.
|
||||
*Ops
|
||||
// Extra seccomp options.
|
||||
Seccomp seccomp.FilterOpts
|
||||
// Seccomp system call filter rules.
|
||||
SeccompRules []seccomp.NativeRule
|
||||
// Extra seccomp flags.
|
||||
SeccompFlags seccomp.ExportFlag
|
||||
// Seccomp presets. Has no effect unless SeccompRules is zero-length.
|
||||
SeccompPresets seccomp.FilterPreset
|
||||
// Do not load seccomp program.
|
||||
SeccompDisable bool
|
||||
// Permission bits of newly created parent directories.
|
||||
// The zero value is interpreted as 0755.
|
||||
ParentPerm os.FileMode
|
||||
// Do not syscall.Setsid.
|
||||
RetainSession bool
|
||||
// Do not [syscall.CLONE_NEWNET].
|
||||
HostNet bool
|
||||
// Retain CAP_SYS_ADMIN.
|
||||
Privileged bool
|
||||
|
||||
Flags HardeningFlags
|
||||
}
|
||||
)
|
||||
|
||||
@@ -117,11 +99,9 @@ func (p *Container) Start() error {
|
||||
ctx, cancel := context.WithCancel(p.ctx)
|
||||
p.cancel = cancel
|
||||
|
||||
var cloneFlags uintptr = syscall.CLONE_NEWIPC |
|
||||
syscall.CLONE_NEWUTS |
|
||||
syscall.CLONE_NEWCGROUP
|
||||
if p.Flags&FAllowNet == 0 {
|
||||
cloneFlags |= syscall.CLONE_NEWNET
|
||||
var cloneFlags uintptr = CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP
|
||||
if !p.HostNet {
|
||||
cloneFlags |= CLONE_NEWNET
|
||||
}
|
||||
|
||||
// map to overflow id to work around ownership checks
|
||||
@@ -132,6 +112,10 @@ func (p *Container) Start() error {
|
||||
p.Gid = OverflowGid()
|
||||
}
|
||||
|
||||
if !p.RetainSession {
|
||||
p.SeccompPresets |= seccomp.PresetDenyTTY
|
||||
}
|
||||
|
||||
if p.CommandContext != nil {
|
||||
p.cmd = p.CommandContext(ctx)
|
||||
} else {
|
||||
@@ -144,17 +128,13 @@ func (p *Container) Start() error {
|
||||
if p.Cancel != nil {
|
||||
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
|
||||
} else {
|
||||
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(syscall.SIGTERM) }
|
||||
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(SIGTERM) }
|
||||
}
|
||||
p.cmd.Dir = "/"
|
||||
p.cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setsid: p.Flags&FAllowTTY == 0,
|
||||
Pdeathsig: syscall.SIGKILL,
|
||||
|
||||
Cloneflags: cloneFlags |
|
||||
syscall.CLONE_NEWUSER |
|
||||
syscall.CLONE_NEWPID |
|
||||
syscall.CLONE_NEWNS,
|
||||
p.cmd.SysProcAttr = &SysProcAttr{
|
||||
Setsid: !p.RetainSession,
|
||||
Pdeathsig: SIGKILL,
|
||||
Cloneflags: cloneFlags | CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS,
|
||||
|
||||
// remain privileged for setup
|
||||
AmbientCaps: []uintptr{CAP_SYS_ADMIN, CAP_SETPCAP},
|
||||
@@ -192,7 +172,7 @@ func (p *Container) Serve() error {
|
||||
|
||||
if p.Path != "" && !path.IsAbs(p.Path) {
|
||||
p.cancel()
|
||||
return msg.WrapErr(syscall.EINVAL,
|
||||
return msg.WrapErr(EINVAL,
|
||||
fmt.Sprintf("invalid executable path %q", p.Path))
|
||||
}
|
||||
|
||||
@@ -201,7 +181,7 @@ func (p *Container) Serve() error {
|
||||
p.Path = os.Getenv("SHELL")
|
||||
if !path.IsAbs(p.Path) {
|
||||
p.cancel()
|
||||
return msg.WrapErr(syscall.EBADE,
|
||||
return msg.WrapErr(EBADE,
|
||||
"no command specified and $SHELL is invalid")
|
||||
}
|
||||
p.name = path.Base(p.Path)
|
||||
@@ -215,11 +195,16 @@ func (p *Container) Serve() error {
|
||||
}
|
||||
}
|
||||
|
||||
if p.SeccompRules == nil {
|
||||
// do not transmit nil
|
||||
p.SeccompRules = make([]seccomp.NativeRule, 0)
|
||||
}
|
||||
|
||||
err := setup.Encode(
|
||||
&initParams{
|
||||
p.Params,
|
||||
syscall.Getuid(),
|
||||
syscall.Getgid(),
|
||||
Getuid(),
|
||||
Getgid(),
|
||||
len(p.ExtraFiles),
|
||||
msg.IsVerbose(),
|
||||
},
|
||||
@@ -233,8 +218,8 @@ func (p *Container) Serve() error {
|
||||
func (p *Container) Wait() error { defer p.cancel(); return p.cmd.Wait() }
|
||||
|
||||
func (p *Container) String() string {
|
||||
return fmt.Sprintf("argv: %q, flags: %#x, seccomp: %#x",
|
||||
p.Args, p.Flags, int(p.Flags.seccomp(p.Seccomp)))
|
||||
return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x",
|
||||
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets))
|
||||
}
|
||||
|
||||
func New(ctx context.Context, name string, args ...string) *Container {
|
||||
@@ -1,4 +1,4 @@
|
||||
package sandbox_test
|
||||
package container_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -12,13 +12,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/fst"
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"git.gensokyo.uk/security/fortify/ldd"
|
||||
"git.gensokyo.uk/security/fortify/sandbox"
|
||||
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||
"git.gensokyo.uk/security/fortify/sandbox/vfs"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/vfs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/ldd"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -28,31 +28,48 @@ const (
|
||||
|
||||
func TestContainer(t *testing.T) {
|
||||
{
|
||||
oldVerbose := fmsg.Load()
|
||||
oldOutput := sandbox.GetOutput()
|
||||
internal.InstallFmsg(true)
|
||||
t.Cleanup(func() { fmsg.Store(oldVerbose) })
|
||||
t.Cleanup(func() { sandbox.SetOutput(oldOutput) })
|
||||
oldVerbose := hlog.Load()
|
||||
oldOutput := container.GetOutput()
|
||||
internal.InstallOutput(true)
|
||||
t.Cleanup(func() { hlog.Store(oldVerbose) })
|
||||
t.Cleanup(func() { container.SetOutput(oldOutput) })
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
flags sandbox.HardeningFlags
|
||||
ops *sandbox.Ops
|
||||
mnt []*vfs.MountInfoEntry
|
||||
host string
|
||||
name string
|
||||
filter bool
|
||||
session bool
|
||||
net bool
|
||||
ops *container.Ops
|
||||
mnt []*vfs.MountInfoEntry
|
||||
host string
|
||||
rules []seccomp.NativeRule
|
||||
flags seccomp.ExportFlag
|
||||
presets seccomp.FilterPreset
|
||||
}{
|
||||
{"minimal", 0, new(sandbox.Ops), nil, "test-minimal"},
|
||||
{"allow", sandbox.FAllowUserns | sandbox.FAllowNet | sandbox.FAllowTTY,
|
||||
new(sandbox.Ops), nil, "test-minimal"},
|
||||
{"tmpfs", 0,
|
||||
new(sandbox.Ops).
|
||||
Tmpfs(fst.Tmp, 0, 0755),
|
||||
{"minimal", true, false, false,
|
||||
new(container.Ops), nil, "test-minimal",
|
||||
nil, 0, seccomp.PresetStrict},
|
||||
{"allow", true, true, true,
|
||||
new(container.Ops), nil, "test-minimal",
|
||||
nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel},
|
||||
{"no filter", false, true, true,
|
||||
new(container.Ops), nil, "test-no-filter",
|
||||
nil, 0, seccomp.PresetExt},
|
||||
{"custom rules", true, true, true,
|
||||
new(container.Ops), nil, "test-no-filter",
|
||||
[]seccomp.NativeRule{
|
||||
{seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil},
|
||||
}, 0, seccomp.PresetExt},
|
||||
{"tmpfs", true, false, false,
|
||||
new(container.Ops).
|
||||
Tmpfs(hst.Tmp, 0, 0755),
|
||||
[]*vfs.MountInfoEntry{
|
||||
e("/", fst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
|
||||
}, "test-tmpfs"},
|
||||
{"dev", sandbox.FAllowTTY, // go test output is not a tty
|
||||
new(sandbox.Ops).
|
||||
e("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
|
||||
}, "test-tmpfs",
|
||||
nil, 0, seccomp.PresetStrict},
|
||||
{"dev", true, true /* go test output is not a tty */, false,
|
||||
new(container.Ops).
|
||||
Dev("/dev").
|
||||
Mqueue("/dev/mqueue"),
|
||||
[]*vfs.MountInfoEntry{
|
||||
@@ -65,37 +82,43 @@ func TestContainer(t *testing.T) {
|
||||
e("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
e("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
|
||||
e("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
|
||||
}, ""},
|
||||
}, "",
|
||||
nil, 0, seccomp.PresetStrict},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
container := sandbox.New(ctx, "/usr/bin/sandbox.test", "-test.v",
|
||||
c := container.New(ctx, "/usr/bin/sandbox.test", "-test.v",
|
||||
"-test.run=TestHelperCheckContainer", "--", "check", tc.host)
|
||||
container.Uid = 1000
|
||||
container.Gid = 100
|
||||
container.Hostname = tc.host
|
||||
container.CommandContext = commandContext
|
||||
container.Flags |= tc.flags
|
||||
container.Stdout, container.Stderr = os.Stdout, os.Stderr
|
||||
container.Ops = tc.ops
|
||||
if container.Args[5] == "" {
|
||||
c.Uid = 1000
|
||||
c.Gid = 100
|
||||
c.Hostname = tc.host
|
||||
c.CommandContext = commandContext
|
||||
c.Stdout, c.Stderr = os.Stdout, os.Stderr
|
||||
c.Ops = tc.ops
|
||||
c.SeccompRules = tc.rules
|
||||
c.SeccompFlags = tc.flags | seccomp.AllowMultiarch
|
||||
c.SeccompPresets = tc.presets
|
||||
c.SeccompDisable = !tc.filter
|
||||
c.RetainSession = tc.session
|
||||
c.HostNet = tc.net
|
||||
if c.Args[5] == "" {
|
||||
if name, err := os.Hostname(); err != nil {
|
||||
t.Fatalf("cannot get hostname: %v", err)
|
||||
} else {
|
||||
container.Args[5] = name
|
||||
c.Args[5] = name
|
||||
}
|
||||
}
|
||||
|
||||
container.
|
||||
c.
|
||||
Tmpfs("/tmp", 0, 0755).
|
||||
Bind(os.Args[0], os.Args[0], 0).
|
||||
Mkdir("/usr/bin", 0755).
|
||||
Link(os.Args[0], "/usr/bin/sandbox.test").
|
||||
Place("/etc/hostname", []byte(container.Args[5]))
|
||||
Place("/etc/hostname", []byte(c.Args[5]))
|
||||
// in case test has cgo enabled
|
||||
var libPaths []string
|
||||
if entries, err := ldd.ExecFilter(ctx,
|
||||
@@ -108,10 +131,10 @@ func TestContainer(t *testing.T) {
|
||||
libPaths = ldd.Path(entries)
|
||||
}
|
||||
for _, name := range libPaths {
|
||||
container.Bind(name, name, 0)
|
||||
c.Bind(name, name, 0)
|
||||
}
|
||||
// needs /proc to check mountinfo
|
||||
container.Proc("/proc")
|
||||
c.Proc("/proc")
|
||||
|
||||
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
|
||||
mnt = append(mnt, e("/sysroot", "/", "rw,nosuid,nodev,relatime", "tmpfs", "rootfs", ignore))
|
||||
@@ -129,17 +152,17 @@ func TestContainer(t *testing.T) {
|
||||
if err := gob.NewEncoder(want).Encode(mnt); err != nil {
|
||||
t.Fatalf("cannot serialise expected mount points: %v", err)
|
||||
}
|
||||
container.Stdin = want
|
||||
c.Stdin = want
|
||||
|
||||
if err := container.Start(); err != nil {
|
||||
fmsg.PrintBaseError(err, "start:")
|
||||
if err := c.Start(); err != nil {
|
||||
hlog.PrintBaseError(err, "start:")
|
||||
t.Fatalf("cannot start container: %v", err)
|
||||
} else if err = container.Serve(); err != nil {
|
||||
fmsg.PrintBaseError(err, "serve:")
|
||||
} else if err = c.Serve(); err != nil {
|
||||
hlog.PrintBaseError(err, "serve:")
|
||||
t.Errorf("cannot serve setup params: %v", err)
|
||||
}
|
||||
if err := container.Wait(); err != nil {
|
||||
fmsg.PrintBaseError(err, "wait:")
|
||||
if err := c.Wait(); err != nil {
|
||||
hlog.PrintBaseError(err, "wait:")
|
||||
t.Fatalf("wait: %v", err)
|
||||
}
|
||||
})
|
||||
@@ -162,11 +185,14 @@ func e(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoE
|
||||
}
|
||||
|
||||
func TestContainerString(t *testing.T) {
|
||||
container := sandbox.New(context.TODO(), "ldd", "/usr/bin/env")
|
||||
container.Flags |= sandbox.FAllowDevel
|
||||
container.Seccomp |= seccomp.FilterMultiarch
|
||||
want := `argv: ["ldd" "/usr/bin/env"], flags: 0x2, seccomp: 0x2e`
|
||||
if got := container.String(); got != want {
|
||||
c := container.New(t.Context(), "ldd", "/usr/bin/env")
|
||||
c.SeccompFlags |= seccomp.AllowMultiarch
|
||||
c.SeccompRules = seccomp.Preset(
|
||||
seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY,
|
||||
c.SeccompFlags)
|
||||
c.SeccompPresets = seccomp.PresetStrict
|
||||
want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf`
|
||||
if got := c.String(); got != want {
|
||||
t.Errorf("String: %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
@@ -175,8 +201,8 @@ func TestHelperInit(t *testing.T) {
|
||||
if len(os.Args) != 5 || os.Args[4] != "init" {
|
||||
return
|
||||
}
|
||||
sandbox.SetOutput(fmsg.Output{})
|
||||
sandbox.Init(fmsg.Prepare, internal.InstallFmsg)
|
||||
container.SetOutput(hlog.Output{})
|
||||
container.Init(hlog.Prepare, internal.InstallOutput)
|
||||
}
|
||||
|
||||
func TestHelperCheckContainer(t *testing.T) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package sandbox
|
||||
package container
|
||||
|
||||
import (
|
||||
"log"
|
||||
@@ -1,15 +1,15 @@
|
||||
package sandbox_test
|
||||
package container_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/sandbox"
|
||||
"hakurei.app/container"
|
||||
)
|
||||
|
||||
func TestExecutable(t *testing.T) {
|
||||
for i := 0; i < 16; i++ {
|
||||
if got := sandbox.MustExecutable(); got != os.Args[0] {
|
||||
if got := container.MustExecutable(); got != os.Args[0] {
|
||||
t.Errorf("MustExecutable: %q, want %q",
|
||||
got, os.Args[0])
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package sandbox
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"path"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"syscall"
|
||||
. "syscall"
|
||||
"time"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||
"hakurei.app/container/seccomp"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -24,7 +24,7 @@ const (
|
||||
basePath = "/tmp"
|
||||
|
||||
// setup params file descriptor
|
||||
setupEnv = "FORTIFY_SETUP"
|
||||
setupEnv = "HAKUREI_SETUP"
|
||||
)
|
||||
|
||||
type initParams struct {
|
||||
@@ -56,7 +56,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
||||
log.Fatal("invalid setup descriptor")
|
||||
}
|
||||
if errors.Is(err, ErrNotSet) {
|
||||
log.Fatal("FORTIFY_SETUP not set")
|
||||
log.Fatal("HAKUREI_SETUP not set")
|
||||
}
|
||||
|
||||
log.Fatalf("cannot decode init setup payload: %v", err)
|
||||
@@ -97,9 +97,9 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
||||
log.Fatalf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||
}
|
||||
|
||||
oldmask := syscall.Umask(0)
|
||||
oldmask := Umask(0)
|
||||
if params.Hostname != "" {
|
||||
if err := syscall.Sethostname([]byte(params.Hostname)); err != nil {
|
||||
if err := Sethostname([]byte(params.Hostname)); err != nil {
|
||||
log.Fatalf("cannot set hostname: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -107,9 +107,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
||||
// cache sysctl before pivot_root
|
||||
LastCap()
|
||||
|
||||
if err := syscall.Mount("", "/", "",
|
||||
syscall.MS_SILENT|syscall.MS_SLAVE|syscall.MS_REC,
|
||||
""); err != nil {
|
||||
if err := Mount("", "/", "", MS_SILENT|MS_SLAVE|MS_REC, ""); err != nil {
|
||||
log.Fatalf("cannot make / rslave: %v", err)
|
||||
}
|
||||
|
||||
@@ -126,9 +124,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := syscall.Mount("rootfs", basePath, "tmpfs",
|
||||
syscall.MS_NODEV|syscall.MS_NOSUID,
|
||||
""); err != nil {
|
||||
if err := Mount("rootfs", basePath, "tmpfs", MS_NODEV|MS_NOSUID, ""); err != nil {
|
||||
log.Fatalf("cannot mount intermediate root: %v", err)
|
||||
}
|
||||
if err := os.Chdir(basePath); err != nil {
|
||||
@@ -138,9 +134,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
||||
if err := os.Mkdir(sysrootDir, 0755); err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
if err := syscall.Mount(sysrootDir, sysrootDir, "",
|
||||
syscall.MS_SILENT|syscall.MS_MGC_VAL|syscall.MS_BIND|syscall.MS_REC,
|
||||
""); err != nil {
|
||||
if err := Mount(sysrootDir, sysrootDir, "", MS_SILENT|MS_MGC_VAL|MS_BIND|MS_REC, ""); err != nil {
|
||||
log.Fatalf("cannot bind sysroot: %v", err)
|
||||
}
|
||||
|
||||
@@ -148,7 +142,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
// pivot_root uncovers basePath in hostDir
|
||||
if err := syscall.PivotRoot(basePath, hostDir); err != nil {
|
||||
if err := PivotRoot(basePath, hostDir); err != nil {
|
||||
log.Fatalf("cannot pivot into intermediate root: %v", err)
|
||||
}
|
||||
if err := os.Chdir("/"); err != nil {
|
||||
@@ -167,19 +161,17 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
||||
}
|
||||
|
||||
// setup requiring host root complete at this point
|
||||
if err := syscall.Mount(hostDir, hostDir, "",
|
||||
syscall.MS_SILENT|syscall.MS_REC|syscall.MS_PRIVATE,
|
||||
""); err != nil {
|
||||
if err := Mount(hostDir, hostDir, "", MS_SILENT|MS_REC|MS_PRIVATE, ""); err != nil {
|
||||
log.Fatalf("cannot make host root rprivate: %v", err)
|
||||
}
|
||||
if err := syscall.Unmount(hostDir, syscall.MNT_DETACH); err != nil {
|
||||
if err := Unmount(hostDir, MNT_DETACH); err != nil {
|
||||
log.Fatalf("cannot unmount host root: %v", err)
|
||||
}
|
||||
|
||||
{
|
||||
var fd int
|
||||
if err := IgnoringEINTR(func() (err error) {
|
||||
fd, err = syscall.Open("/", syscall.O_DIRECTORY|syscall.O_RDONLY, 0)
|
||||
fd, err = Open("/", O_DIRECTORY|O_RDONLY, 0)
|
||||
return
|
||||
}); err != nil {
|
||||
log.Fatalf("cannot open intermediate root: %v", err)
|
||||
@@ -188,36 +180,36 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
if err := syscall.PivotRoot(".", "."); err != nil {
|
||||
if err := PivotRoot(".", "."); err != nil {
|
||||
log.Fatalf("cannot pivot into sysroot: %v", err)
|
||||
}
|
||||
if err := syscall.Fchdir(fd); err != nil {
|
||||
if err := Fchdir(fd); err != nil {
|
||||
log.Fatalf("cannot re-enter intermediate root: %v", err)
|
||||
}
|
||||
if err := syscall.Unmount(".", syscall.MNT_DETACH); err != nil {
|
||||
if err := Unmount(".", MNT_DETACH); err != nil {
|
||||
log.Fatalf("cannot unmount intemediate root: %v", err)
|
||||
}
|
||||
if err := os.Chdir("/"); err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
if err := syscall.Close(fd); err != nil {
|
||||
if err := Close(fd); err != nil {
|
||||
log.Fatalf("cannot close intermediate root: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, _, errno := syscall.Syscall(PR_SET_NO_NEW_PRIVS, 1, 0, 0); errno != 0 {
|
||||
if _, _, errno := Syscall(PR_SET_NO_NEW_PRIVS, 1, 0, 0); errno != 0 {
|
||||
log.Fatalf("prctl(PR_SET_NO_NEW_PRIVS): %v", errno)
|
||||
}
|
||||
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0); errno != 0 {
|
||||
if _, _, errno := Syscall(SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0); errno != 0 {
|
||||
log.Fatalf("cannot clear the ambient capability set: %v", errno)
|
||||
}
|
||||
for i := uintptr(0); i <= LastCap(); i++ {
|
||||
if params.Privileged && i == CAP_SYS_ADMIN {
|
||||
continue
|
||||
}
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_CAPBSET_DROP, i, 0); errno != 0 {
|
||||
if _, _, errno := Syscall(SYS_PRCTL, PR_CAPBSET_DROP, i, 0); errno != 0 {
|
||||
log.Fatalf("cannot drop capability from bonding set: %v", errno)
|
||||
}
|
||||
}
|
||||
@@ -226,7 +218,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
||||
if params.Privileged {
|
||||
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
|
||||
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_SYS_ADMIN); errno != 0 {
|
||||
if _, _, errno := Syscall(SYS_PRCTL, PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_SYS_ADMIN); errno != 0 {
|
||||
log.Fatalf("cannot raise CAP_SYS_ADMIN: %v", errno)
|
||||
}
|
||||
}
|
||||
@@ -237,8 +229,18 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
||||
log.Fatalf("cannot capset: %v", err)
|
||||
}
|
||||
|
||||
if err := seccomp.Load(params.Flags.seccomp(params.Seccomp)); err != nil {
|
||||
log.Fatalf("cannot load syscall filter: %v", err)
|
||||
if !params.SeccompDisable {
|
||||
rules := params.SeccompRules
|
||||
if len(rules) == 0 { // non-empty rules slice always overrides presets
|
||||
msg.Verbosef("resolving presets %#x", params.SeccompPresets)
|
||||
rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags)
|
||||
}
|
||||
if err := seccomp.Load(rules, params.SeccompFlags); err != nil {
|
||||
log.Fatalf("cannot load syscall filter: %v", err)
|
||||
}
|
||||
msg.Verbosef("%d filter rules loaded", len(rules))
|
||||
} else {
|
||||
msg.Verbose("syscall filter not configured")
|
||||
}
|
||||
|
||||
extraFiles := make([]*os.File, params.Count)
|
||||
@@ -246,7 +248,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
||||
// setup fd is placed before all extra files
|
||||
extraFiles[i] = os.NewFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
|
||||
}
|
||||
syscall.Umask(oldmask)
|
||||
Umask(oldmask)
|
||||
|
||||
cmd := exec.Command(params.Path)
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
@@ -267,7 +269,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
||||
|
||||
type winfo struct {
|
||||
wpid int
|
||||
wstatus syscall.WaitStatus
|
||||
wstatus WaitStatus
|
||||
}
|
||||
info := make(chan winfo, 1)
|
||||
done := make(chan struct{})
|
||||
@@ -276,7 +278,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
||||
var (
|
||||
err error
|
||||
wpid = -2
|
||||
wstatus syscall.WaitStatus
|
||||
wstatus WaitStatus
|
||||
)
|
||||
|
||||
// keep going until no child process is left
|
||||
@@ -289,12 +291,12 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
||||
info <- winfo{wpid, wstatus}
|
||||
}
|
||||
|
||||
err = syscall.EINTR
|
||||
for errors.Is(err, syscall.EINTR) {
|
||||
wpid, err = syscall.Wait4(-1, &wstatus, 0, nil)
|
||||
err = EINTR
|
||||
for errors.Is(err, EINTR) {
|
||||
wpid, err = Wait4(-1, &wstatus, 0, nil)
|
||||
}
|
||||
}
|
||||
if !errors.Is(err, syscall.ECHILD) {
|
||||
if !errors.Is(err, ECHILD) {
|
||||
log.Println("unexpected wait4 response:", err)
|
||||
}
|
||||
|
||||
@@ -303,7 +305,7 @@ func Init(prepare func(prefix string), setVerbose func(verbose bool)) {
|
||||
|
||||
// handle signals to dump withheld messages
|
||||
sig := make(chan os.Signal, 2)
|
||||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||
signal.Notify(sig, SIGINT, SIGTERM)
|
||||
|
||||
// closed after residualProcessTimeout has elapsed after initial process death
|
||||
timeout := make(chan struct{})
|
||||
@@ -1,13 +1,13 @@
|
||||
package sandbox
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
. "syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/sandbox/vfs"
|
||||
"hakurei.app/container/vfs"
|
||||
)
|
||||
|
||||
func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) error {
|
||||
@@ -17,8 +17,7 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
|
||||
msg.Verbosef("resolved %q on %q flags %#x", source, target, flags)
|
||||
}
|
||||
|
||||
if err := syscall.Mount(source, target, "",
|
||||
syscall.MS_SILENT|syscall.MS_BIND|flags&syscall.MS_REC, ""); err != nil {
|
||||
if err := Mount(source, target, "", MS_SILENT|MS_BIND|flags&MS_REC, ""); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot mount %q on %q:", source, target))
|
||||
}
|
||||
@@ -38,7 +37,7 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
|
||||
{
|
||||
var destFd int
|
||||
if err := IgnoringEINTR(func() (err error) {
|
||||
destFd, err = syscall.Open(targetFinal, O_PATH|syscall.O_CLOEXEC, 0)
|
||||
destFd, err = Open(targetFinal, O_PATH|O_CLOEXEC, 0)
|
||||
return
|
||||
}); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
@@ -46,7 +45,7 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
|
||||
}
|
||||
if v, err := os.Readlink(p.fd(destFd)); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
} else if err = syscall.Close(destFd); err != nil {
|
||||
} else if err = Close(destFd); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot close %q:", targetFinal))
|
||||
} else {
|
||||
@@ -54,11 +53,11 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
|
||||
}
|
||||
}
|
||||
|
||||
mf := syscall.MS_NOSUID | flags&syscall.MS_NODEV | flags&syscall.MS_RDONLY
|
||||
mf := MS_NOSUID | flags&MS_NODEV | flags&MS_RDONLY
|
||||
return hostProc.mountinfo(func(d *vfs.MountInfoDecoder) error {
|
||||
n, err := d.Unfold(targetKFinal)
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.ESTALE) {
|
||||
if errors.Is(err, ESTALE) {
|
||||
return msg.WrapErr(err,
|
||||
fmt.Sprintf("mount point %q never appeared in mountinfo", targetKFinal))
|
||||
}
|
||||
@@ -69,13 +68,13 @@ func (p *procPaths) bindMount(source, target string, flags uintptr, eq bool) err
|
||||
if err = remountWithFlags(n, mf); err != nil {
|
||||
return err
|
||||
}
|
||||
if flags&syscall.MS_REC == 0 {
|
||||
if flags&MS_REC == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for cur := range n.Collective() {
|
||||
err = remountWithFlags(cur, mf)
|
||||
if err != nil && !errors.Is(err, syscall.EACCES) {
|
||||
if err != nil && !errors.Is(err, EACCES) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -91,9 +90,8 @@ func remountWithFlags(n *vfs.MountInfoNode, mf uintptr) error {
|
||||
}
|
||||
|
||||
if kf&mf != mf {
|
||||
return wrapErrSuffix(syscall.Mount("none", n.Clean, "",
|
||||
syscall.MS_SILENT|syscall.MS_BIND|syscall.MS_REMOUNT|kf|mf,
|
||||
""),
|
||||
return wrapErrSuffix(
|
||||
Mount("none", n.Clean, "", MS_SILENT|MS_BIND|MS_REMOUNT|kf|mf, ""),
|
||||
fmt.Sprintf("cannot remount %q:", n.Clean))
|
||||
}
|
||||
return nil
|
||||
@@ -108,8 +106,8 @@ func mountTmpfs(fsname, name string, size int, perm os.FileMode) error {
|
||||
if size > 0 {
|
||||
opt += fmt.Sprintf(",size=%d", size)
|
||||
}
|
||||
return wrapErrSuffix(syscall.Mount(fsname, target, "tmpfs",
|
||||
syscall.MS_NOSUID|syscall.MS_NODEV, opt),
|
||||
return wrapErrSuffix(
|
||||
Mount(fsname, target, "tmpfs", MS_NOSUID|MS_NODEV, opt),
|
||||
fmt.Sprintf("cannot mount tmpfs on %q:", name))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package sandbox
|
||||
package container
|
||||
|
||||
import (
|
||||
"log"
|
||||
@@ -1,4 +1,4 @@
|
||||
package sandbox
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
. "syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
@@ -29,10 +29,10 @@ type (
|
||||
|
||||
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
|
||||
|
||||
func init() { gob.Register(new(BindMount)) }
|
||||
func init() { gob.Register(new(BindMountOp)) }
|
||||
|
||||
// BindMount bind mounts host path Source on container path Target.
|
||||
type BindMount struct {
|
||||
// BindMountOp bind mounts host path Source on container path Target.
|
||||
type BindMountOp struct {
|
||||
Source, SourceFinal, Target string
|
||||
|
||||
Flags int
|
||||
@@ -44,10 +44,9 @@ const (
|
||||
BindDevice
|
||||
)
|
||||
|
||||
func (b *BindMount) early(*Params) error {
|
||||
func (b *BindMountOp) early(*Params) error {
|
||||
if !path.IsAbs(b.Source) {
|
||||
return msg.WrapErr(syscall.EBADE,
|
||||
fmt.Sprintf("path %q is not absolute", b.Source))
|
||||
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", b.Source))
|
||||
}
|
||||
|
||||
if v, err := filepath.EvalSymlinks(b.Source); err != nil {
|
||||
@@ -62,18 +61,17 @@ func (b *BindMount) early(*Params) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BindMount) apply(*Params) error {
|
||||
func (b *BindMountOp) apply(*Params) error {
|
||||
if b.SourceFinal == "\x00" {
|
||||
if b.Flags&BindOptional == 0 {
|
||||
// unreachable
|
||||
return syscall.EBADE
|
||||
return EBADE
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if !path.IsAbs(b.SourceFinal) || !path.IsAbs(b.Target) {
|
||||
return msg.WrapErr(syscall.EBADE,
|
||||
"path is not absolute")
|
||||
return msg.WrapErr(EBADE, "path is not absolute")
|
||||
}
|
||||
|
||||
source := toHost(b.SourceFinal)
|
||||
@@ -91,73 +89,70 @@ func (b *BindMount) apply(*Params) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var flags uintptr = syscall.MS_REC
|
||||
var flags uintptr = MS_REC
|
||||
if b.Flags&BindWritable == 0 {
|
||||
flags |= syscall.MS_RDONLY
|
||||
flags |= MS_RDONLY
|
||||
}
|
||||
if b.Flags&BindDevice == 0 {
|
||||
flags |= syscall.MS_NODEV
|
||||
flags |= MS_NODEV
|
||||
}
|
||||
|
||||
return hostProc.bindMount(source, target, flags, b.SourceFinal == b.Target)
|
||||
}
|
||||
|
||||
func (b *BindMount) Is(op Op) bool { vb, ok := op.(*BindMount); return ok && *b == *vb }
|
||||
func (*BindMount) prefix() string { return "mounting" }
|
||||
func (b *BindMount) String() string {
|
||||
func (b *BindMountOp) Is(op Op) bool { vb, ok := op.(*BindMountOp); return ok && *b == *vb }
|
||||
func (*BindMountOp) prefix() string { return "mounting" }
|
||||
func (b *BindMountOp) String() string {
|
||||
if b.Source == b.Target {
|
||||
return fmt.Sprintf("%q flags %#x", b.Source, b.Flags)
|
||||
}
|
||||
return fmt.Sprintf("%q on %q flags %#x", b.Source, b.Target, b.Flags&BindWritable)
|
||||
}
|
||||
func (f *Ops) Bind(source, target string, flags int) *Ops {
|
||||
*f = append(*f, &BindMount{source, "", target, flags})
|
||||
*f = append(*f, &BindMountOp{source, "", target, flags})
|
||||
return f
|
||||
}
|
||||
|
||||
func init() { gob.Register(new(MountProc)) }
|
||||
func init() { gob.Register(new(MountProcOp)) }
|
||||
|
||||
// MountProc mounts a private instance of proc.
|
||||
type MountProc string
|
||||
// MountProcOp mounts a private instance of proc.
|
||||
type MountProcOp string
|
||||
|
||||
func (p MountProc) early(*Params) error { return nil }
|
||||
func (p MountProc) apply(params *Params) error {
|
||||
func (p MountProcOp) early(*Params) error { return nil }
|
||||
func (p MountProcOp) apply(params *Params) error {
|
||||
v := string(p)
|
||||
|
||||
if !path.IsAbs(v) {
|
||||
return msg.WrapErr(syscall.EBADE,
|
||||
fmt.Sprintf("path %q is not absolute", v))
|
||||
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v))
|
||||
}
|
||||
|
||||
target := toSysroot(v)
|
||||
if err := os.MkdirAll(target, params.ParentPerm); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
return wrapErrSuffix(syscall.Mount("proc", target, "proc",
|
||||
syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""),
|
||||
return wrapErrSuffix(Mount("proc", target, "proc", MS_NOSUID|MS_NOEXEC|MS_NODEV, ""),
|
||||
fmt.Sprintf("cannot mount proc on %q:", v))
|
||||
}
|
||||
|
||||
func (p MountProc) Is(op Op) bool { vp, ok := op.(MountProc); return ok && p == vp }
|
||||
func (MountProc) prefix() string { return "mounting" }
|
||||
func (p MountProc) String() string { return fmt.Sprintf("proc on %q", string(p)) }
|
||||
func (p MountProcOp) Is(op Op) bool { vp, ok := op.(MountProcOp); return ok && p == vp }
|
||||
func (MountProcOp) prefix() string { return "mounting" }
|
||||
func (p MountProcOp) String() string { return fmt.Sprintf("proc on %q", string(p)) }
|
||||
func (f *Ops) Proc(dest string) *Ops {
|
||||
*f = append(*f, MountProc(dest))
|
||||
*f = append(*f, MountProcOp(dest))
|
||||
return f
|
||||
}
|
||||
|
||||
func init() { gob.Register(new(MountDev)) }
|
||||
func init() { gob.Register(new(MountDevOp)) }
|
||||
|
||||
// MountDev mounts part of host dev.
|
||||
type MountDev string
|
||||
// MountDevOp mounts part of host dev.
|
||||
type MountDevOp string
|
||||
|
||||
func (d MountDev) early(*Params) error { return nil }
|
||||
func (d MountDev) apply(params *Params) error {
|
||||
func (d MountDevOp) early(*Params) error { return nil }
|
||||
func (d MountDevOp) apply(params *Params) error {
|
||||
v := string(d)
|
||||
|
||||
if !path.IsAbs(v) {
|
||||
return msg.WrapErr(syscall.EBADE,
|
||||
fmt.Sprintf("path %q is not absolute", v))
|
||||
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v))
|
||||
}
|
||||
target := toSysroot(v)
|
||||
|
||||
@@ -204,19 +199,15 @@ func (d MountDev) apply(params *Params) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := syscall.Mount("devpts", devPtsPath, "devpts",
|
||||
syscall.MS_NOSUID|syscall.MS_NOEXEC,
|
||||
if err := Mount("devpts", devPtsPath, "devpts", MS_NOSUID|MS_NOEXEC,
|
||||
"newinstance,ptmxmode=0666,mode=620"); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
fmt.Sprintf("cannot mount devpts on %q:", devPtsPath))
|
||||
}
|
||||
|
||||
if params.Flags&FAllowTTY != 0 {
|
||||
if params.RetainSession {
|
||||
var buf [8]byte
|
||||
if _, _, errno := syscall.Syscall(
|
||||
syscall.SYS_IOCTL, 1, syscall.TIOCGWINSZ,
|
||||
uintptr(unsafe.Pointer(&buf[0])),
|
||||
); errno == 0 {
|
||||
if _, _, errno := Syscall(SYS_IOCTL, 1, TIOCGWINSZ, uintptr(unsafe.Pointer(&buf[0]))); errno == 0 {
|
||||
consolePath := toSysroot(path.Join(v, "console"))
|
||||
if err := ensureFile(consolePath, 0444, params.ParentPerm); err != nil {
|
||||
return err
|
||||
@@ -237,86 +228,81 @@ func (d MountDev) apply(params *Params) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d MountDev) Is(op Op) bool { vd, ok := op.(MountDev); return ok && d == vd }
|
||||
func (MountDev) prefix() string { return "mounting" }
|
||||
func (d MountDev) String() string { return fmt.Sprintf("dev on %q", string(d)) }
|
||||
func (d MountDevOp) Is(op Op) bool { vd, ok := op.(MountDevOp); return ok && d == vd }
|
||||
func (MountDevOp) prefix() string { return "mounting" }
|
||||
func (d MountDevOp) String() string { return fmt.Sprintf("dev on %q", string(d)) }
|
||||
func (f *Ops) Dev(dest string) *Ops {
|
||||
*f = append(*f, MountDev(dest))
|
||||
*f = append(*f, MountDevOp(dest))
|
||||
return f
|
||||
}
|
||||
|
||||
func init() { gob.Register(new(MountMqueue)) }
|
||||
func init() { gob.Register(new(MountMqueueOp)) }
|
||||
|
||||
// MountMqueue mounts a private mqueue instance on container Path.
|
||||
type MountMqueue string
|
||||
// MountMqueueOp mounts a private mqueue instance on container Path.
|
||||
type MountMqueueOp string
|
||||
|
||||
func (m MountMqueue) early(*Params) error { return nil }
|
||||
func (m MountMqueue) apply(params *Params) error {
|
||||
func (m MountMqueueOp) early(*Params) error { return nil }
|
||||
func (m MountMqueueOp) apply(params *Params) error {
|
||||
v := string(m)
|
||||
|
||||
if !path.IsAbs(v) {
|
||||
return msg.WrapErr(syscall.EBADE,
|
||||
fmt.Sprintf("path %q is not absolute", v))
|
||||
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", v))
|
||||
}
|
||||
|
||||
target := toSysroot(v)
|
||||
if err := os.MkdirAll(target, params.ParentPerm); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
}
|
||||
return wrapErrSuffix(syscall.Mount("mqueue", target, "mqueue",
|
||||
syscall.MS_NOSUID|syscall.MS_NOEXEC|syscall.MS_NODEV, ""),
|
||||
return wrapErrSuffix(Mount("mqueue", target, "mqueue", MS_NOSUID|MS_NOEXEC|MS_NODEV, ""),
|
||||
fmt.Sprintf("cannot mount mqueue on %q:", v))
|
||||
}
|
||||
|
||||
func (m MountMqueue) Is(op Op) bool { vm, ok := op.(MountMqueue); return ok && m == vm }
|
||||
func (MountMqueue) prefix() string { return "mounting" }
|
||||
func (m MountMqueue) String() string { return fmt.Sprintf("mqueue on %q", string(m)) }
|
||||
func (m MountMqueueOp) Is(op Op) bool { vm, ok := op.(MountMqueueOp); return ok && m == vm }
|
||||
func (MountMqueueOp) prefix() string { return "mounting" }
|
||||
func (m MountMqueueOp) String() string { return fmt.Sprintf("mqueue on %q", string(m)) }
|
||||
func (f *Ops) Mqueue(dest string) *Ops {
|
||||
*f = append(*f, MountMqueue(dest))
|
||||
*f = append(*f, MountMqueueOp(dest))
|
||||
return f
|
||||
}
|
||||
|
||||
func init() { gob.Register(new(MountTmpfs)) }
|
||||
func init() { gob.Register(new(MountTmpfsOp)) }
|
||||
|
||||
// MountTmpfs mounts tmpfs on container Path.
|
||||
type MountTmpfs struct {
|
||||
// MountTmpfsOp mounts tmpfs on container Path.
|
||||
type MountTmpfsOp struct {
|
||||
Path string
|
||||
Size int
|
||||
Perm os.FileMode
|
||||
}
|
||||
|
||||
func (t *MountTmpfs) early(*Params) error { return nil }
|
||||
func (t *MountTmpfs) apply(*Params) error {
|
||||
func (t *MountTmpfsOp) early(*Params) error { return nil }
|
||||
func (t *MountTmpfsOp) apply(*Params) error {
|
||||
if !path.IsAbs(t.Path) {
|
||||
return msg.WrapErr(syscall.EBADE,
|
||||
fmt.Sprintf("path %q is not absolute", t.Path))
|
||||
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path))
|
||||
}
|
||||
if t.Size < 0 || t.Size > math.MaxUint>>1 {
|
||||
return msg.WrapErr(syscall.EBADE,
|
||||
fmt.Sprintf("size %d out of bounds", t.Size))
|
||||
return msg.WrapErr(EBADE, fmt.Sprintf("size %d out of bounds", t.Size))
|
||||
}
|
||||
return mountTmpfs("tmpfs", t.Path, t.Size, t.Perm)
|
||||
}
|
||||
|
||||
func (t *MountTmpfs) Is(op Op) bool { vt, ok := op.(*MountTmpfs); return ok && *t == *vt }
|
||||
func (*MountTmpfs) prefix() string { return "mounting" }
|
||||
func (t *MountTmpfs) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
|
||||
func (t *MountTmpfsOp) Is(op Op) bool { vt, ok := op.(*MountTmpfsOp); return ok && *t == *vt }
|
||||
func (*MountTmpfsOp) prefix() string { return "mounting" }
|
||||
func (t *MountTmpfsOp) String() string { return fmt.Sprintf("tmpfs on %q size %d", t.Path, t.Size) }
|
||||
func (f *Ops) Tmpfs(dest string, size int, perm os.FileMode) *Ops {
|
||||
*f = append(*f, &MountTmpfs{dest, size, perm})
|
||||
*f = append(*f, &MountTmpfsOp{dest, size, perm})
|
||||
return f
|
||||
}
|
||||
|
||||
func init() { gob.Register(new(Symlink)) }
|
||||
func init() { gob.Register(new(SymlinkOp)) }
|
||||
|
||||
// Symlink creates a symlink in the container filesystem.
|
||||
type Symlink [2]string
|
||||
// SymlinkOp creates a symlink in the container filesystem.
|
||||
type SymlinkOp [2]string
|
||||
|
||||
func (l *Symlink) early(*Params) error {
|
||||
func (l *SymlinkOp) early(*Params) error {
|
||||
if strings.HasPrefix(l[0], "*") {
|
||||
l[0] = l[0][1:]
|
||||
if !path.IsAbs(l[0]) {
|
||||
return msg.WrapErr(syscall.EBADE,
|
||||
fmt.Sprintf("path %q is not absolute", l[0]))
|
||||
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[0]))
|
||||
}
|
||||
if name, err := os.Readlink(l[0]); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
@@ -326,11 +312,10 @@ func (l *Symlink) early(*Params) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (l *Symlink) apply(params *Params) error {
|
||||
func (l *SymlinkOp) apply(params *Params) error {
|
||||
// symlink target is an arbitrary path value, so only validate link name here
|
||||
if !path.IsAbs(l[1]) {
|
||||
return msg.WrapErr(syscall.EBADE,
|
||||
fmt.Sprintf("path %q is not absolute", l[1]))
|
||||
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", l[1]))
|
||||
}
|
||||
|
||||
target := toSysroot(l[1])
|
||||
@@ -343,27 +328,26 @@ func (l *Symlink) apply(params *Params) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Symlink) Is(op Op) bool { vl, ok := op.(*Symlink); return ok && *l == *vl }
|
||||
func (*Symlink) prefix() string { return "creating" }
|
||||
func (l *Symlink) String() string { return fmt.Sprintf("symlink on %q target %q", l[1], l[0]) }
|
||||
func (l *SymlinkOp) Is(op Op) bool { vl, ok := op.(*SymlinkOp); return ok && *l == *vl }
|
||||
func (*SymlinkOp) prefix() string { return "creating" }
|
||||
func (l *SymlinkOp) String() string { return fmt.Sprintf("symlink on %q target %q", l[1], l[0]) }
|
||||
func (f *Ops) Link(target, linkName string) *Ops {
|
||||
*f = append(*f, &Symlink{target, linkName})
|
||||
*f = append(*f, &SymlinkOp{target, linkName})
|
||||
return f
|
||||
}
|
||||
|
||||
func init() { gob.Register(new(Mkdir)) }
|
||||
func init() { gob.Register(new(MkdirOp)) }
|
||||
|
||||
// Mkdir creates a directory in the container filesystem.
|
||||
type Mkdir struct {
|
||||
// MkdirOp creates a directory in the container filesystem.
|
||||
type MkdirOp struct {
|
||||
Path string
|
||||
Perm os.FileMode
|
||||
}
|
||||
|
||||
func (m *Mkdir) early(*Params) error { return nil }
|
||||
func (m *Mkdir) apply(*Params) error {
|
||||
func (m *MkdirOp) early(*Params) error { return nil }
|
||||
func (m *MkdirOp) apply(*Params) error {
|
||||
if !path.IsAbs(m.Path) {
|
||||
return msg.WrapErr(syscall.EBADE,
|
||||
fmt.Sprintf("path %q is not absolute", m.Path))
|
||||
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", m.Path))
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(toSysroot(m.Path), m.Perm); err != nil {
|
||||
@@ -372,27 +356,26 @@ func (m *Mkdir) apply(*Params) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Mkdir) Is(op Op) bool { vm, ok := op.(*Mkdir); return ok && m == vm }
|
||||
func (*Mkdir) prefix() string { return "creating" }
|
||||
func (m *Mkdir) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
|
||||
func (m *MkdirOp) Is(op Op) bool { vm, ok := op.(*MkdirOp); return ok && m == vm }
|
||||
func (*MkdirOp) prefix() string { return "creating" }
|
||||
func (m *MkdirOp) String() string { return fmt.Sprintf("directory %q perm %s", m.Path, m.Perm) }
|
||||
func (f *Ops) Mkdir(dest string, perm os.FileMode) *Ops {
|
||||
*f = append(*f, &Mkdir{dest, perm})
|
||||
*f = append(*f, &MkdirOp{dest, perm})
|
||||
return f
|
||||
}
|
||||
|
||||
func init() { gob.Register(new(Tmpfile)) }
|
||||
func init() { gob.Register(new(TmpfileOp)) }
|
||||
|
||||
// Tmpfile places a file in container Path containing Data.
|
||||
type Tmpfile struct {
|
||||
// TmpfileOp places a file in container Path containing Data.
|
||||
type TmpfileOp struct {
|
||||
Path string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (t *Tmpfile) early(*Params) error { return nil }
|
||||
func (t *Tmpfile) apply(params *Params) error {
|
||||
func (t *TmpfileOp) early(*Params) error { return nil }
|
||||
func (t *TmpfileOp) apply(params *Params) error {
|
||||
if !path.IsAbs(t.Path) {
|
||||
return msg.WrapErr(syscall.EBADE,
|
||||
fmt.Sprintf("path %q is not absolute", t.Path))
|
||||
return msg.WrapErr(EBADE, fmt.Sprintf("path %q is not absolute", t.Path))
|
||||
}
|
||||
|
||||
var tmpPath string
|
||||
@@ -414,7 +397,7 @@ func (t *Tmpfile) apply(params *Params) error {
|
||||
} else if err = hostProc.bindMount(
|
||||
tmpPath,
|
||||
target,
|
||||
syscall.MS_RDONLY|syscall.MS_NODEV,
|
||||
MS_RDONLY|MS_NODEV,
|
||||
false,
|
||||
); err != nil {
|
||||
return err
|
||||
@@ -424,31 +407,31 @@ func (t *Tmpfile) apply(params *Params) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Tmpfile) Is(op Op) bool {
|
||||
vt, ok := op.(*Tmpfile)
|
||||
func (t *TmpfileOp) Is(op Op) bool {
|
||||
vt, ok := op.(*TmpfileOp)
|
||||
return ok && t.Path == vt.Path && slices.Equal(t.Data, vt.Data)
|
||||
}
|
||||
func (*Tmpfile) prefix() string { return "placing" }
|
||||
func (t *Tmpfile) String() string {
|
||||
func (*TmpfileOp) prefix() string { return "placing" }
|
||||
func (t *TmpfileOp) String() string {
|
||||
return fmt.Sprintf("tmpfile %q (%d bytes)", t.Path, len(t.Data))
|
||||
}
|
||||
func (f *Ops) Place(name string, data []byte) *Ops { *f = append(*f, &Tmpfile{name, data}); return f }
|
||||
func (f *Ops) Place(name string, data []byte) *Ops { *f = append(*f, &TmpfileOp{name, data}); return f }
|
||||
func (f *Ops) PlaceP(name string, dataP **[]byte) *Ops {
|
||||
t := &Tmpfile{Path: name}
|
||||
t := &TmpfileOp{Path: name}
|
||||
*dataP = &t.Data
|
||||
|
||||
*f = append(*f, t)
|
||||
return f
|
||||
}
|
||||
|
||||
func init() { gob.Register(new(AutoEtc)) }
|
||||
func init() { gob.Register(new(AutoEtcOp)) }
|
||||
|
||||
// AutoEtc expands host /etc into a toplevel symlink mirror with /etc semantics.
|
||||
// AutoEtcOp expands host /etc into a toplevel symlink mirror with /etc semantics.
|
||||
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
|
||||
type AutoEtc struct{ Prefix string }
|
||||
type AutoEtcOp struct{ Prefix string }
|
||||
|
||||
func (e *AutoEtc) early(*Params) error { return nil }
|
||||
func (e *AutoEtc) apply(*Params) error {
|
||||
func (e *AutoEtcOp) early(*Params) error { return nil }
|
||||
func (e *AutoEtcOp) apply(*Params) error {
|
||||
const target = sysrootPath + "/etc/"
|
||||
rel := e.hostRel() + "/"
|
||||
|
||||
@@ -481,17 +464,17 @@ func (e *AutoEtc) apply(*Params) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
func (e *AutoEtc) hostPath() string { return "/etc/" + e.hostRel() }
|
||||
func (e *AutoEtc) hostRel() string { return ".host/" + e.Prefix }
|
||||
func (e *AutoEtcOp) hostPath() string { return "/etc/" + e.hostRel() }
|
||||
func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
|
||||
|
||||
func (e *AutoEtc) Is(op Op) bool {
|
||||
ve, ok := op.(*AutoEtc)
|
||||
func (e *AutoEtcOp) Is(op Op) bool {
|
||||
ve, ok := op.(*AutoEtcOp)
|
||||
return ok && ((e == nil && ve == nil) || (e != nil && ve != nil && *e == *ve))
|
||||
}
|
||||
func (*AutoEtc) prefix() string { return "setting up" }
|
||||
func (e *AutoEtc) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }
|
||||
func (*AutoEtcOp) prefix() string { return "setting up" }
|
||||
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }
|
||||
func (f *Ops) Etc(host, prefix string) *Ops {
|
||||
e := &AutoEtc{prefix}
|
||||
e := &AutoEtcOp{prefix}
|
||||
f.Mkdir("/etc", 0755)
|
||||
f.Bind(host, e.hostPath(), 0)
|
||||
*f = append(*f, e)
|
||||
@@ -1,4 +1,4 @@
|
||||
package sandbox
|
||||
package container
|
||||
|
||||
var msg Msg = new(DefaultMsg)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package sandbox
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
@@ -1,4 +1,4 @@
|
||||
package sandbox
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/sandbox/vfs"
|
||||
"hakurei.app/container/vfs"
|
||||
)
|
||||
|
||||
const (
|
||||
130
container/seccomp/libseccomp-helper.c
Normal file
130
container/seccomp/libseccomp-helper.c
Normal file
@@ -0,0 +1,130 @@
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE /* CLONE_NEWUSER */
|
||||
#endif
|
||||
|
||||
#include "libseccomp-helper.h"
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))
|
||||
|
||||
int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch,
|
||||
uint32_t multiarch,
|
||||
struct hakurei_syscall_rule *rules,
|
||||
size_t rules_sz, hakurei_export_flag flags) {
|
||||
int i;
|
||||
int last_allowed_family;
|
||||
int disallowed;
|
||||
struct hakurei_syscall_rule *rule;
|
||||
|
||||
int32_t res = 0; /* refer to resPrefix for message */
|
||||
|
||||
/* Blocklist all but unix, inet, inet6 and netlink */
|
||||
struct {
|
||||
int family;
|
||||
hakurei_export_flag flags_mask;
|
||||
} socket_family_allowlist[] = {
|
||||
/* NOTE: Keep in numerical order */
|
||||
{AF_UNSPEC, 0},
|
||||
{AF_LOCAL, 0},
|
||||
{AF_INET, 0},
|
||||
{AF_INET6, 0},
|
||||
{AF_NETLINK, 0},
|
||||
{AF_CAN, HAKUREI_EXPORT_CAN},
|
||||
{AF_BLUETOOTH, HAKUREI_EXPORT_BLUETOOTH},
|
||||
};
|
||||
|
||||
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
|
||||
if (ctx == NULL) {
|
||||
res = 1;
|
||||
goto out;
|
||||
} else
|
||||
errno = 0;
|
||||
|
||||
/* We only really need to handle arches on multiarch systems.
|
||||
* If only one arch is supported the default is fine */
|
||||
if (arch != 0) {
|
||||
/* This *adds* the target arch, instead of replacing the
|
||||
* native one. This is not ideal, because we'd like to only
|
||||
* allow the target arch, but we can't really disallow the
|
||||
* native arch at this point, because then bubblewrap
|
||||
* couldn't continue running. */
|
||||
*ret_p = seccomp_arch_add(ctx, arch);
|
||||
if (*ret_p < 0 && *ret_p != -EEXIST) {
|
||||
res = 2;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (flags & HAKUREI_EXPORT_MULTIARCH && multiarch != 0) {
|
||||
*ret_p = seccomp_arch_add(ctx, multiarch);
|
||||
if (*ret_p < 0 && *ret_p != -EEXIST) {
|
||||
res = 3;
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < rules_sz; i++) {
|
||||
rule = &rules[i];
|
||||
assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS);
|
||||
|
||||
if (rule->arg)
|
||||
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno),
|
||||
rule->syscall, 1, *rule->arg);
|
||||
else
|
||||
*ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno),
|
||||
rule->syscall, 0);
|
||||
|
||||
if (*ret_p == -EFAULT) {
|
||||
res = 4;
|
||||
goto out;
|
||||
} else if (*ret_p < 0) {
|
||||
res = 5;
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
/* Socket filtering doesn't work on e.g. i386, so ignore failures here
|
||||
* However, we need to user seccomp_rule_add_exact to avoid libseccomp doing
|
||||
* something else: https://github.com/seccomp/libseccomp/issues/8 */
|
||||
last_allowed_family = -1;
|
||||
for (i = 0; i < LEN(socket_family_allowlist); i++) {
|
||||
if (socket_family_allowlist[i].flags_mask != 0 &&
|
||||
(socket_family_allowlist[i].flags_mask & flags) !=
|
||||
socket_family_allowlist[i].flags_mask)
|
||||
continue;
|
||||
|
||||
for (disallowed = last_allowed_family + 1;
|
||||
disallowed < socket_family_allowlist[i].family; disallowed++) {
|
||||
/* Blocklist the in-between valid families */
|
||||
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT),
|
||||
SCMP_SYS(socket), 1,
|
||||
SCMP_A0(SCMP_CMP_EQ, disallowed));
|
||||
}
|
||||
last_allowed_family = socket_family_allowlist[i].family;
|
||||
}
|
||||
/* Blocklist the rest */
|
||||
seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1,
|
||||
SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));
|
||||
|
||||
if (fd < 0) {
|
||||
*ret_p = seccomp_load(ctx);
|
||||
if (*ret_p != 0) {
|
||||
res = 7;
|
||||
goto out;
|
||||
}
|
||||
} else {
|
||||
*ret_p = seccomp_export_bpf(ctx, fd);
|
||||
if (*ret_p != 0) {
|
||||
res = 6;
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
out:
|
||||
if (ctx)
|
||||
seccomp_release(ctx);
|
||||
|
||||
return res;
|
||||
}
|
||||
24
container/seccomp/libseccomp-helper.h
Normal file
24
container/seccomp/libseccomp-helper.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#include <seccomp.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#if (SCMP_VER_MAJOR < 2) || (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 5) || \
|
||||
(SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR == 5 && SCMP_VER_MICRO < 1)
|
||||
#error This package requires libseccomp >= v2.5.1
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
HAKUREI_EXPORT_MULTIARCH = 1 << 0,
|
||||
HAKUREI_EXPORT_CAN = 1 << 1,
|
||||
HAKUREI_EXPORT_BLUETOOTH = 1 << 2,
|
||||
} hakurei_export_flag;
|
||||
|
||||
struct hakurei_syscall_rule {
|
||||
int syscall;
|
||||
int m_errno;
|
||||
struct scmp_arg_cmp *arg;
|
||||
};
|
||||
|
||||
int32_t hakurei_export_filter(int *ret_p, int fd, uint32_t arch,
|
||||
uint32_t multiarch,
|
||||
struct hakurei_syscall_rule *rules,
|
||||
size_t rules_sz, hakurei_export_flag flags);
|
||||
188
container/seccomp/libseccomp.go
Normal file
188
container/seccomp/libseccomp.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package seccomp
|
||||
|
||||
/*
|
||||
#cgo linux pkg-config: --static libseccomp
|
||||
|
||||
#include <libseccomp-helper.h>
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidRules = errors.New("invalid native rules slice")
|
||||
)
|
||||
|
||||
// LibraryError represents a libseccomp error.
|
||||
type LibraryError struct {
|
||||
Prefix string
|
||||
Seccomp syscall.Errno
|
||||
Errno error
|
||||
}
|
||||
|
||||
func (e *LibraryError) Error() string {
|
||||
if e.Seccomp == 0 {
|
||||
if e.Errno == nil {
|
||||
panic("invalid libseccomp error")
|
||||
}
|
||||
return fmt.Sprintf("%s: %s", e.Prefix, e.Errno)
|
||||
}
|
||||
if e.Errno == nil {
|
||||
return fmt.Sprintf("%s: %s", e.Prefix, e.Seccomp)
|
||||
}
|
||||
return fmt.Sprintf("%s: %s (%s)", e.Prefix, e.Seccomp, e.Errno)
|
||||
}
|
||||
|
||||
func (e *LibraryError) Is(err error) bool {
|
||||
if e == nil {
|
||||
return err == nil
|
||||
}
|
||||
if ef, ok := err.(*LibraryError); ok {
|
||||
return *e == *ef
|
||||
}
|
||||
return (e.Seccomp != 0 && errors.Is(err, e.Seccomp)) ||
|
||||
(e.Errno != nil && errors.Is(err, e.Errno))
|
||||
}
|
||||
|
||||
type (
|
||||
ScmpSyscall = C.int
|
||||
ScmpErrno = C.int
|
||||
)
|
||||
|
||||
// A NativeRule specifies an arch-specific action taken by seccomp under certain conditions.
|
||||
type NativeRule struct {
|
||||
// Syscall is the arch-dependent syscall number to act against.
|
||||
Syscall ScmpSyscall
|
||||
// Errno is the errno value to return when the condition is satisfied.
|
||||
Errno ScmpErrno
|
||||
// Arg is the optional struct scmp_arg_cmp passed to libseccomp.
|
||||
Arg *ScmpArgCmp
|
||||
}
|
||||
|
||||
type ExportFlag = C.hakurei_export_flag
|
||||
|
||||
const (
|
||||
// AllowMultiarch allows multiarch/emulation.
|
||||
AllowMultiarch ExportFlag = C.HAKUREI_EXPORT_MULTIARCH
|
||||
// AllowCAN allows AF_CAN.
|
||||
AllowCAN ExportFlag = C.HAKUREI_EXPORT_CAN
|
||||
// AllowBluetooth allows AF_BLUETOOTH.
|
||||
AllowBluetooth ExportFlag = C.HAKUREI_EXPORT_BLUETOOTH
|
||||
)
|
||||
|
||||
var resPrefix = [...]string{
|
||||
0: "",
|
||||
1: "seccomp_init failed",
|
||||
2: "seccomp_arch_add failed",
|
||||
3: "seccomp_arch_add failed (multiarch)",
|
||||
4: "internal libseccomp failure",
|
||||
5: "seccomp_rule_add failed",
|
||||
6: "seccomp_export_bpf failed",
|
||||
7: "seccomp_load failed",
|
||||
}
|
||||
|
||||
// Export streams filter contents to fd, or installs it to the current process if fd < 0.
|
||||
func Export(fd int, rules []NativeRule, flags ExportFlag) error {
|
||||
if len(rules) == 0 {
|
||||
return ErrInvalidRules
|
||||
}
|
||||
|
||||
var (
|
||||
arch C.uint32_t = 0
|
||||
multiarch C.uint32_t = 0
|
||||
)
|
||||
switch runtime.GOARCH {
|
||||
case "386":
|
||||
arch = C.SCMP_ARCH_X86
|
||||
case "amd64":
|
||||
arch = C.SCMP_ARCH_X86_64
|
||||
multiarch = C.SCMP_ARCH_X86
|
||||
case "arm":
|
||||
arch = C.SCMP_ARCH_ARM
|
||||
case "arm64":
|
||||
arch = C.SCMP_ARCH_AARCH64
|
||||
multiarch = C.SCMP_ARCH_ARM
|
||||
}
|
||||
|
||||
var ret C.int
|
||||
|
||||
rulesPinner := new(runtime.Pinner)
|
||||
for i := range rules {
|
||||
rule := &rules[i]
|
||||
rulesPinner.Pin(rule)
|
||||
if rule.Arg != nil {
|
||||
rulesPinner.Pin(rule.Arg)
|
||||
}
|
||||
}
|
||||
res, err := C.hakurei_export_filter(
|
||||
&ret, C.int(fd),
|
||||
arch, multiarch,
|
||||
(*C.struct_hakurei_syscall_rule)(unsafe.Pointer(&rules[0])),
|
||||
C.size_t(len(rules)),
|
||||
flags,
|
||||
)
|
||||
rulesPinner.Unpin()
|
||||
|
||||
if prefix := resPrefix[res]; prefix != "" {
|
||||
return &LibraryError{
|
||||
prefix,
|
||||
-syscall.Errno(ret),
|
||||
err,
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ScmpCompare is the equivalent of scmp_compare;
|
||||
// Comparison operators
|
||||
type ScmpCompare = C.enum_scmp_compare
|
||||
|
||||
const (
|
||||
_SCMP_CMP_MIN = C._SCMP_CMP_MIN
|
||||
|
||||
// not equal
|
||||
SCMP_CMP_NE = C.SCMP_CMP_NE
|
||||
// less than
|
||||
SCMP_CMP_LT = C.SCMP_CMP_LT
|
||||
// less than or equal
|
||||
SCMP_CMP_LE = C.SCMP_CMP_LE
|
||||
// equal
|
||||
SCMP_CMP_EQ = C.SCMP_CMP_EQ
|
||||
// greater than or equal
|
||||
SCMP_CMP_GE = C.SCMP_CMP_GE
|
||||
// greater than
|
||||
SCMP_CMP_GT = C.SCMP_CMP_GT
|
||||
// masked equality
|
||||
SCMP_CMP_MASKED_EQ = C.SCMP_CMP_MASKED_EQ
|
||||
|
||||
_SCMP_CMP_MAX = C._SCMP_CMP_MAX
|
||||
)
|
||||
|
||||
// ScmpDatum is the equivalent of scmp_datum_t;
|
||||
// Argument datum
|
||||
type ScmpDatum uint64
|
||||
|
||||
// ScmpArgCmp is the equivalent of struct scmp_arg_cmp;
|
||||
// Argument / Value comparison definition
|
||||
type ScmpArgCmp struct {
|
||||
// argument number, starting at 0
|
||||
Arg C.uint
|
||||
// the comparison op, e.g. SCMP_CMP_*
|
||||
Op ScmpCompare
|
||||
|
||||
DatumA, DatumB ScmpDatum
|
||||
}
|
||||
|
||||
// only used for testing
|
||||
func syscallResolveName(s string) (trap int) {
|
||||
v := C.CString(s)
|
||||
trap = int(C.seccomp_syscall_resolve_name(v))
|
||||
C.free(unsafe.Pointer(v))
|
||||
|
||||
return
|
||||
}
|
||||
@@ -8,17 +8,18 @@ import (
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||
. "hakurei.app/container/seccomp"
|
||||
)
|
||||
|
||||
func TestExport(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
opts seccomp.FilterOpts
|
||||
presets FilterPreset
|
||||
flags ExportFlag
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{"compat", 0, []byte{
|
||||
{"compat", 0, 0, []byte{
|
||||
0x95, 0xec, 0x69, 0xd0, 0x17, 0x73, 0x3e, 0x07,
|
||||
0x21, 0x60, 0xe0, 0xda, 0x80, 0xfd, 0xeb, 0xec,
|
||||
0xdf, 0x27, 0xae, 0x81, 0x66, 0xf5, 0xe2, 0xa7,
|
||||
@@ -28,7 +29,7 @@ func TestExport(t *testing.T) {
|
||||
0xa7, 0x9b, 0x07, 0x0e, 0x04, 0xc0, 0xee, 0x9a,
|
||||
0xcd, 0xf5, 0x8f, 0x55, 0xcf, 0xa8, 0x15, 0xa5,
|
||||
}, false},
|
||||
{"base", seccomp.FilterExt, []byte{
|
||||
{"base", PresetExt, 0, []byte{
|
||||
0xdc, 0x7f, 0x2e, 0x1c, 0x5e, 0x82, 0x9b, 0x79,
|
||||
0xeb, 0xb7, 0xef, 0xc7, 0x59, 0x15, 0x0f, 0x54,
|
||||
0xa8, 0x3a, 0x75, 0xc8, 0xdf, 0x6f, 0xee, 0x4d,
|
||||
@@ -38,10 +39,10 @@ func TestExport(t *testing.T) {
|
||||
0x1d, 0xb0, 0x5d, 0x90, 0x99, 0x7c, 0x86, 0x59,
|
||||
0xb9, 0x58, 0x91, 0x20, 0x6a, 0xc9, 0x95, 0x2d,
|
||||
}, false},
|
||||
{"everything", seccomp.FilterExt |
|
||||
seccomp.FilterDenyNS | seccomp.FilterDenyTTY | seccomp.FilterDenyDevel |
|
||||
seccomp.FilterMultiarch | seccomp.FilterLinux32 | seccomp.FilterCan |
|
||||
seccomp.FilterBluetooth, []byte{
|
||||
{"everything", PresetExt |
|
||||
PresetDenyNS | PresetDenyTTY | PresetDenyDevel |
|
||||
PresetLinux32, AllowMultiarch | AllowCAN |
|
||||
AllowBluetooth, []byte{
|
||||
0xe9, 0x9d, 0xd3, 0x45, 0xe1, 0x95, 0x41, 0x34,
|
||||
0x73, 0xd3, 0xcb, 0xee, 0x07, 0xb4, 0xed, 0x57,
|
||||
0xb9, 0x08, 0xbf, 0xa8, 0x9e, 0xa2, 0x07, 0x2f,
|
||||
@@ -51,7 +52,7 @@ func TestExport(t *testing.T) {
|
||||
0x4c, 0x02, 0x4e, 0xd4, 0x88, 0x50, 0xbe, 0x69,
|
||||
0xb6, 0x8a, 0x9a, 0x4c, 0x5f, 0x53, 0xa9, 0xdb,
|
||||
}, false},
|
||||
{"strict", seccomp.PresetStrict, []byte{
|
||||
{"strict", PresetStrict, 0, []byte{
|
||||
0xe8, 0x80, 0x29, 0x8d, 0xf2, 0xbd, 0x67, 0x51,
|
||||
0xd0, 0x04, 0x0f, 0xc2, 0x1b, 0xc0, 0xed, 0x4c,
|
||||
0x00, 0xf9, 0x5d, 0xc0, 0xd7, 0xba, 0x50, 0x6c,
|
||||
@@ -62,7 +63,7 @@ func TestExport(t *testing.T) {
|
||||
0x14, 0x89, 0x60, 0xfb, 0xd3, 0x5c, 0xd7, 0x35,
|
||||
}, false},
|
||||
{"strict compat", 0 |
|
||||
seccomp.FilterDenyNS | seccomp.FilterDenyTTY | seccomp.FilterDenyDevel, []byte{
|
||||
PresetDenyNS | PresetDenyTTY | PresetDenyDevel, 0, []byte{
|
||||
0x39, 0x87, 0x1b, 0x93, 0xff, 0xaf, 0xc8, 0xb9,
|
||||
0x79, 0xfc, 0xed, 0xc0, 0xb0, 0xc3, 0x7b, 0x9e,
|
||||
0x03, 0x92, 0x2f, 0x5b, 0x02, 0x74, 0x8d, 0xc5,
|
||||
@@ -72,7 +73,7 @@ func TestExport(t *testing.T) {
|
||||
0x80, 0x8b, 0x1a, 0x6f, 0x84, 0xf3, 0x2b, 0xbd,
|
||||
0xe1, 0xaa, 0x02, 0xae, 0x30, 0xee, 0xdc, 0xfa,
|
||||
}, false},
|
||||
{"fortify default", seccomp.FilterExt | seccomp.FilterDenyDevel, []byte{
|
||||
{"hakurei default", PresetExt | PresetDenyDevel, 0, []byte{
|
||||
0xc6, 0x98, 0xb0, 0x81, 0xff, 0x95, 0x7a, 0xfe,
|
||||
0x17, 0xa6, 0xd9, 0x43, 0x74, 0x53, 0x7d, 0x37,
|
||||
0xf2, 0xa6, 0x3f, 0x6f, 0x9d, 0xd7, 0x5d, 0xa7,
|
||||
@@ -87,11 +88,7 @@ func TestExport(t *testing.T) {
|
||||
buf := make([]byte, 8)
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
oldF := seccomp.GetOutput()
|
||||
seccomp.SetOutput(t.Log)
|
||||
t.Cleanup(func() { seccomp.SetOutput(oldF) })
|
||||
|
||||
e := seccomp.New(tc.opts)
|
||||
e := New(Preset(tc.presets, tc.flags), tc.flags)
|
||||
digest := sha512.New()
|
||||
|
||||
if _, err := io.CopyBuffer(digest, e, buf); (err != nil) != tc.wantErr {
|
||||
@@ -100,7 +97,6 @@ func TestExport(t *testing.T) {
|
||||
}
|
||||
if err := e.Close(); err != nil {
|
||||
t.Errorf("Close: error = %v", err)
|
||||
return
|
||||
}
|
||||
if got := digest.Sum(nil); !slices.Equal(got, tc.want) {
|
||||
t.Fatalf("Export() hash = %x, want %x",
|
||||
@@ -111,7 +107,7 @@ func TestExport(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("close without use", func(t *testing.T) {
|
||||
e := seccomp.New(0)
|
||||
e := New(Preset(0, 0), 0)
|
||||
if err := e.Close(); !errors.Is(err, syscall.EINVAL) {
|
||||
t.Errorf("Close: error = %v", err)
|
||||
return
|
||||
@@ -119,7 +115,7 @@ func TestExport(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("close partial read", func(t *testing.T) {
|
||||
e := seccomp.New(0)
|
||||
e := New(Preset(0, 0), 0)
|
||||
if _, err := e.Read(nil); err != nil {
|
||||
t.Errorf("Read: error = %v", err)
|
||||
return
|
||||
@@ -137,10 +133,10 @@ func TestExport(t *testing.T) {
|
||||
func BenchmarkExport(b *testing.B) {
|
||||
buf := make([]byte, 8)
|
||||
for i := 0; i < b.N; i++ {
|
||||
e := seccomp.New(seccomp.FilterExt |
|
||||
seccomp.FilterDenyNS | seccomp.FilterDenyTTY | seccomp.FilterDenyDevel |
|
||||
seccomp.FilterMultiarch | seccomp.FilterLinux32 | seccomp.FilterCan |
|
||||
seccomp.FilterBluetooth)
|
||||
e := New(
|
||||
Preset(PresetExt|PresetDenyNS|PresetDenyTTY|PresetDenyDevel|PresetLinux32,
|
||||
AllowMultiarch|AllowCAN|AllowBluetooth),
|
||||
AllowMultiarch|AllowCAN|AllowBluetooth)
|
||||
if _, err := io.CopyBuffer(io.Discard, e, buf); err != nil {
|
||||
b.Fatalf("cannot export: %v", err)
|
||||
}
|
||||
83
container/seccomp/mksysnum_linux.pl
Executable file
83
container/seccomp/mksysnum_linux.pl
Executable file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env perl
|
||||
# Copyright 2009 The Go Authors. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style
|
||||
# license that can be found in the LICENSE file.
|
||||
|
||||
use strict;
|
||||
|
||||
my $command = "mksysnum_linux.pl ". join(' ', @ARGV);
|
||||
|
||||
print <<EOF;
|
||||
// $command
|
||||
// Code generated by the command above; DO NOT EDIT.
|
||||
|
||||
package seccomp
|
||||
|
||||
import . "syscall"
|
||||
|
||||
var syscallNum = map[string]int{
|
||||
EOF
|
||||
|
||||
my $offset = 0;
|
||||
my $state = -1;
|
||||
|
||||
sub fmt {
|
||||
my ($name, $num) = @_;
|
||||
if($num > 999){
|
||||
# ignore deprecated syscalls that are no longer implemented
|
||||
# https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/include/uapi/asm-generic/unistd.h?id=refs/heads/master#n716
|
||||
return;
|
||||
}
|
||||
(my $name_upper = $name) =~ y/a-z/A-Z/;
|
||||
$num = $num + $offset;
|
||||
if($num > 302){ # not wired in Go standard library
|
||||
if($state < 0){
|
||||
print " \"$name\": SYS_$name_upper,\n";
|
||||
}
|
||||
else{
|
||||
print " SYS_$name_upper = $num;\n";
|
||||
}
|
||||
}
|
||||
elsif($state < 0){
|
||||
print " \"$name\": SYS_$name_upper,\n";
|
||||
}
|
||||
else{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
GENERATE:
|
||||
|
||||
my $prev;
|
||||
open(GCC, "gcc -E -dD $ARGV[0] |") || die "can't run gcc";
|
||||
while(<GCC>){
|
||||
if(/^#define __NR_Linux\s+([0-9]+)/){
|
||||
# mips/mips64: extract offset
|
||||
$offset = $1;
|
||||
}
|
||||
elsif(/^#define __NR_syscalls\s+/) {
|
||||
# ignore redefinitions of __NR_syscalls
|
||||
}
|
||||
elsif(/^#define __NR_(\w+)\s+([0-9]+)/){
|
||||
$prev = $2;
|
||||
fmt($1, $2);
|
||||
}
|
||||
elsif(/^#define __NR3264_(\w+)\s+([0-9]+)/){
|
||||
$prev = $2;
|
||||
fmt($1, $2);
|
||||
}
|
||||
elsif(/^#define __NR_(\w+)\s+\(\w+\+\s*([0-9]+)\)/){
|
||||
fmt($1, $prev+$2)
|
||||
}
|
||||
elsif(/^#define __NR_(\w+)\s+\(__NR_Linux \+ ([0-9]+)/){
|
||||
fmt($1, $2);
|
||||
}
|
||||
}
|
||||
|
||||
if($state < 0){
|
||||
$state = $state + 1;
|
||||
print "}\n\nconst (\n";
|
||||
goto GENERATE;
|
||||
}
|
||||
|
||||
print ")";
|
||||
229
container/seccomp/presets.go
Normal file
229
container/seccomp/presets.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package seccomp
|
||||
|
||||
/* flatpak commit 4c3bf179e2e4a2a298cd1db1d045adaf3f564532 */
|
||||
|
||||
import (
|
||||
. "syscall"
|
||||
)
|
||||
|
||||
type FilterPreset int
|
||||
|
||||
const (
|
||||
// PresetExt are project-specific extensions.
|
||||
PresetExt FilterPreset = 1 << iota
|
||||
// PresetDenyNS denies namespace setup syscalls.
|
||||
PresetDenyNS
|
||||
// PresetDenyTTY denies faking input.
|
||||
PresetDenyTTY
|
||||
// PresetDenyDevel denies development-related syscalls.
|
||||
PresetDenyDevel
|
||||
// PresetLinux32 sets PER_LINUX32.
|
||||
PresetLinux32
|
||||
)
|
||||
|
||||
func Preset(presets FilterPreset, flags ExportFlag) (rules []NativeRule) {
|
||||
allowedPersonality := PER_LINUX
|
||||
if presets&PresetLinux32 != 0 {
|
||||
allowedPersonality = PER_LINUX32
|
||||
}
|
||||
presetDevelFinal := presetDevel(ScmpDatum(allowedPersonality))
|
||||
|
||||
l := len(presetCommon)
|
||||
if presets&PresetDenyNS != 0 {
|
||||
l += len(presetNamespace)
|
||||
}
|
||||
if presets&PresetDenyTTY != 0 {
|
||||
l += len(presetTTY)
|
||||
}
|
||||
if presets&PresetDenyDevel != 0 {
|
||||
l += len(presetDevelFinal)
|
||||
}
|
||||
if flags&AllowMultiarch == 0 {
|
||||
l += len(presetEmu)
|
||||
}
|
||||
if presets&PresetExt != 0 {
|
||||
l += len(presetCommonExt)
|
||||
if presets&PresetDenyNS != 0 {
|
||||
l += len(presetNamespaceExt)
|
||||
}
|
||||
if flags&AllowMultiarch == 0 {
|
||||
l += len(presetEmuExt)
|
||||
}
|
||||
}
|
||||
|
||||
rules = make([]NativeRule, 0, l)
|
||||
rules = append(rules, presetCommon...)
|
||||
if presets&PresetDenyNS != 0 {
|
||||
rules = append(rules, presetNamespace...)
|
||||
}
|
||||
if presets&PresetDenyTTY != 0 {
|
||||
rules = append(rules, presetTTY...)
|
||||
}
|
||||
if presets&PresetDenyDevel != 0 {
|
||||
rules = append(rules, presetDevelFinal...)
|
||||
}
|
||||
if flags&AllowMultiarch == 0 {
|
||||
rules = append(rules, presetEmu...)
|
||||
}
|
||||
if presets&PresetExt != 0 {
|
||||
rules = append(rules, presetCommonExt...)
|
||||
if presets&PresetDenyNS != 0 {
|
||||
rules = append(rules, presetNamespaceExt...)
|
||||
}
|
||||
if flags&AllowMultiarch == 0 {
|
||||
rules = append(rules, presetEmuExt...)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
presetCommon = []NativeRule{
|
||||
/* Block dmesg */
|
||||
{ScmpSyscall(SYS_SYSLOG), ScmpErrno(EPERM), nil},
|
||||
/* Useless old syscall */
|
||||
{ScmpSyscall(SYS_USELIB), ScmpErrno(EPERM), nil},
|
||||
/* Don't allow disabling accounting */
|
||||
{ScmpSyscall(SYS_ACCT), ScmpErrno(EPERM), nil},
|
||||
/* Don't allow reading current quota use */
|
||||
{ScmpSyscall(SYS_QUOTACTL), ScmpErrno(EPERM), nil},
|
||||
|
||||
/* Don't allow access to the kernel keyring */
|
||||
{ScmpSyscall(SYS_ADD_KEY), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_KEYCTL), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_REQUEST_KEY), ScmpErrno(EPERM), nil},
|
||||
|
||||
/* Scary VM/NUMA ops */
|
||||
{ScmpSyscall(SYS_MOVE_PAGES), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_MBIND), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_GET_MEMPOLICY), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_SET_MEMPOLICY), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_MIGRATE_PAGES), ScmpErrno(EPERM), nil},
|
||||
}
|
||||
|
||||
/* hakurei: project-specific extensions */
|
||||
presetCommonExt = []NativeRule{
|
||||
/* system calls for changing the system clock */
|
||||
{ScmpSyscall(SYS_ADJTIMEX), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_CLOCK_ADJTIME), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_CLOCK_ADJTIME64), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_CLOCK_SETTIME), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_CLOCK_SETTIME64), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_SETTIMEOFDAY), ScmpErrno(EPERM), nil},
|
||||
|
||||
/* loading and unloading of kernel modules */
|
||||
{ScmpSyscall(SYS_DELETE_MODULE), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_FINIT_MODULE), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_INIT_MODULE), ScmpErrno(EPERM), nil},
|
||||
|
||||
/* system calls for rebooting and reboot preparation */
|
||||
{ScmpSyscall(SYS_KEXEC_FILE_LOAD), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_KEXEC_LOAD), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_REBOOT), ScmpErrno(EPERM), nil},
|
||||
|
||||
/* system calls for enabling/disabling swap devices */
|
||||
{ScmpSyscall(SYS_SWAPOFF), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_SWAPON), ScmpErrno(EPERM), nil},
|
||||
}
|
||||
|
||||
presetNamespace = []NativeRule{
|
||||
/* Don't allow subnamespace setups: */
|
||||
{ScmpSyscall(SYS_UNSHARE), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_SETNS), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_MOUNT), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_UMOUNT), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_UMOUNT2), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_PIVOT_ROOT), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_CHROOT), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_CLONE), ScmpErrno(EPERM),
|
||||
&ScmpArgCmp{cloneArg, SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER}},
|
||||
|
||||
/* seccomp can't look into clone3()'s struct clone_args to check whether
|
||||
* the flags are OK, so we have no choice but to block clone3().
|
||||
* Return ENOSYS so user-space will fall back to clone().
|
||||
* (CVE-2021-41133; see also https://github.com/moby/moby/commit/9f6b562d)
|
||||
*/
|
||||
{ScmpSyscall(SYS_CLONE3), ScmpErrno(ENOSYS), nil},
|
||||
|
||||
/* New mount manipulation APIs can also change our VFS. There's no
|
||||
* legitimate reason to do these in the sandbox, so block all of them
|
||||
* rather than thinking about which ones might be dangerous.
|
||||
* (CVE-2021-41133) */
|
||||
{ScmpSyscall(SYS_OPEN_TREE), ScmpErrno(ENOSYS), nil},
|
||||
{ScmpSyscall(SYS_MOVE_MOUNT), ScmpErrno(ENOSYS), nil},
|
||||
{ScmpSyscall(SYS_FSOPEN), ScmpErrno(ENOSYS), nil},
|
||||
{ScmpSyscall(SYS_FSCONFIG), ScmpErrno(ENOSYS), nil},
|
||||
{ScmpSyscall(SYS_FSMOUNT), ScmpErrno(ENOSYS), nil},
|
||||
{ScmpSyscall(SYS_FSPICK), ScmpErrno(ENOSYS), nil},
|
||||
{ScmpSyscall(SYS_MOUNT_SETATTR), ScmpErrno(ENOSYS), nil},
|
||||
}
|
||||
|
||||
/* hakurei: project-specific extensions */
|
||||
presetNamespaceExt = []NativeRule{
|
||||
/* changing file ownership */
|
||||
{ScmpSyscall(SYS_CHOWN), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_CHOWN32), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_FCHOWN), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_FCHOWN32), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_FCHOWNAT), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_LCHOWN), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_LCHOWN32), ScmpErrno(EPERM), nil},
|
||||
|
||||
/* system calls for changing user ID and group ID credentials */
|
||||
{ScmpSyscall(SYS_SETGID), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_SETGID32), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_SETGROUPS), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_SETGROUPS32), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_SETREGID), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_SETREGID32), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_SETRESGID), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_SETRESGID32), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_SETRESUID), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_SETRESUID32), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_SETREUID), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_SETREUID32), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_SETUID), ScmpErrno(EPERM), nil},
|
||||
{ScmpSyscall(SYS_SETUID32), ScmpErrno(EPERM), nil},
|
||||
}
|
||||
|
||||
presetTTY = []NativeRule{
|
||||
/* Don't allow faking input to the controlling tty (CVE-2017-5226) */
|
||||
{ScmpSyscall(SYS_IOCTL), ScmpErrno(EPERM),
|
||||
&ScmpArgCmp{1, SCMP_CMP_MASKED_EQ, 0xFFFFFFFF, TIOCSTI}},
|
||||
/* In the unlikely event that the controlling tty is a Linux virtual
|
||||
* console (/dev/tty2 or similar), copy/paste operations have an effect
|
||||
* similar to TIOCSTI (CVE-2023-28100) */
|
||||
{ScmpSyscall(SYS_IOCTL), ScmpErrno(EPERM),
|
||||
&ScmpArgCmp{1, SCMP_CMP_MASKED_EQ, 0xFFFFFFFF, TIOCLINUX}},
|
||||
}
|
||||
|
||||
presetEmu = []NativeRule{
|
||||
/* modify_ldt is a historic source of interesting information leaks,
|
||||
* so it's disabled as a hardening measure.
|
||||
* However, it is required to run old 16-bit applications
|
||||
* as well as some Wine patches, so it's allowed in multiarch. */
|
||||
{ScmpSyscall(SYS_MODIFY_LDT), ScmpErrno(EPERM), nil},
|
||||
}
|
||||
|
||||
/* hakurei: project-specific extensions */
|
||||
presetEmuExt = []NativeRule{
|
||||
{ScmpSyscall(SYS_SUBPAGE_PROT), ScmpErrno(ENOSYS), nil},
|
||||
{ScmpSyscall(SYS_SWITCH_ENDIAN), ScmpErrno(ENOSYS), nil},
|
||||
{ScmpSyscall(SYS_VM86), ScmpErrno(ENOSYS), nil},
|
||||
{ScmpSyscall(SYS_VM86OLD), ScmpErrno(ENOSYS), nil},
|
||||
}
|
||||
)
|
||||
|
||||
func presetDevel(allowedPersonality ScmpDatum) []NativeRule {
|
||||
return []NativeRule{
|
||||
/* Profiling operations; we expect these to be done by tools from outside
|
||||
* the sandbox. In particular perf has been the source of many CVEs. */
|
||||
{ScmpSyscall(SYS_PERF_EVENT_OPEN), ScmpErrno(EPERM), nil},
|
||||
/* Don't allow you to switch to bsd emulation or whatnot */
|
||||
{ScmpSyscall(SYS_PERSONALITY), ScmpErrno(EPERM),
|
||||
&ScmpArgCmp{0, SCMP_CMP_NE, allowedPersonality, 0}},
|
||||
|
||||
{ScmpSyscall(SYS_PTRACE), ScmpErrno(EPERM), nil},
|
||||
}
|
||||
}
|
||||
7
container/seccomp/presets_clone_backwards2.go
Normal file
7
container/seccomp/presets_clone_backwards2.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build s390 || s390x
|
||||
|
||||
package seccomp
|
||||
|
||||
/* Architectures with CONFIG_CLONE_BACKWARDS2: the child stack
|
||||
* and flags arguments are reversed so the flags come second */
|
||||
const cloneArg = 1
|
||||
6
container/seccomp/presets_clone_generic.go
Normal file
6
container/seccomp/presets_clone_generic.go
Normal file
@@ -0,0 +1,6 @@
|
||||
//go:build !s390 && !s390x
|
||||
|
||||
package seccomp
|
||||
|
||||
/* Normally the flags come first */
|
||||
const cloneArg = 0
|
||||
@@ -5,19 +5,18 @@ import (
|
||||
"errors"
|
||||
"syscall"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||
"hakurei.app/helper/proc"
|
||||
)
|
||||
|
||||
const (
|
||||
PresetStrict = FilterExt | FilterDenyNS | FilterDenyTTY | FilterDenyDevel
|
||||
PresetCommon = PresetStrict | FilterMultiarch
|
||||
PresetStrict = PresetExt | PresetDenyNS | PresetDenyTTY | PresetDenyDevel
|
||||
)
|
||||
|
||||
// New returns an inactive Encoder instance.
|
||||
func New(opts FilterOpts) *Encoder { return &Encoder{newExporter(opts)} }
|
||||
func New(rules []NativeRule, flags ExportFlag) *Encoder { return &Encoder{newExporter(rules, flags)} }
|
||||
|
||||
// Load loads a filter into the kernel.
|
||||
func Load(opts FilterOpts) error { return buildFilter(-1, opts) }
|
||||
func Load(rules []NativeRule, flags ExportFlag) error { return Export(-1, rules, flags) }
|
||||
|
||||
/*
|
||||
An Encoder writes a BPF program to an output stream.
|
||||
@@ -47,17 +46,20 @@ func (e *Encoder) Close() error {
|
||||
}
|
||||
|
||||
// NewFile returns an instance of exporter implementing [proc.File].
|
||||
func NewFile(opts FilterOpts) proc.File { return &File{opts: opts} }
|
||||
func NewFile(rules []NativeRule, flags ExportFlag) proc.File {
|
||||
return &File{rules: rules, flags: flags}
|
||||
}
|
||||
|
||||
// File implements [proc.File] and provides access to the read end of exporter pipe.
|
||||
type File struct {
|
||||
opts FilterOpts
|
||||
rules []NativeRule
|
||||
flags ExportFlag
|
||||
proc.BaseFile
|
||||
}
|
||||
|
||||
func (f *File) ErrCount() int { return 2 }
|
||||
func (f *File) Fulfill(ctx context.Context, dispatchErr func(error)) error {
|
||||
e := newExporter(f.opts)
|
||||
e := newExporter(f.rules, f.flags)
|
||||
if err := e.prepare(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
// Package seccomp provides high level wrappers around libseccomp.
|
||||
package seccomp
|
||||
|
||||
import (
|
||||
@@ -7,8 +8,9 @@ import (
|
||||
)
|
||||
|
||||
type exporter struct {
|
||||
opts FilterOpts
|
||||
r, w *os.File
|
||||
rules []NativeRule
|
||||
flags ExportFlag
|
||||
r, w *os.File
|
||||
|
||||
prepareOnce sync.Once
|
||||
prepareErr error
|
||||
@@ -28,7 +30,7 @@ func (e *exporter) prepare() error {
|
||||
|
||||
ec := make(chan error, 1)
|
||||
go func(fd uintptr) {
|
||||
ec <- buildFilter(int(fd), e.opts)
|
||||
ec <- Export(int(fd), e.rules, e.flags)
|
||||
close(ec)
|
||||
_ = e.closeWrite()
|
||||
runtime.KeepAlive(e.w)
|
||||
@@ -53,6 +55,6 @@ func (e *exporter) closeWrite() error {
|
||||
return e.closeErr
|
||||
}
|
||||
|
||||
func newExporter(opts FilterOpts) *exporter {
|
||||
return &exporter{opts: opts}
|
||||
func newExporter(rules []NativeRule, flags ExportFlag) *exporter {
|
||||
return &exporter{rules: rules, flags: flags}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/sandbox/seccomp"
|
||||
"hakurei.app/container/seccomp"
|
||||
)
|
||||
|
||||
func TestLibraryError(t *testing.T) {
|
||||
28
container/seccomp/syscall.go
Normal file
28
container/seccomp/syscall.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package seccomp
|
||||
|
||||
import "iter"
|
||||
|
||||
// Syscalls returns an iterator over all wired syscalls.
|
||||
func Syscalls() iter.Seq2[string, int] {
|
||||
return func(yield func(string, int) bool) {
|
||||
for name, num := range syscallNum {
|
||||
if !yield(name, num) {
|
||||
return
|
||||
}
|
||||
}
|
||||
for name, num := range syscallNumExtra {
|
||||
if !yield(name, num) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SyscallResolveName resolves a syscall number from its string representation.
|
||||
func SyscallResolveName(name string) (num int, ok bool) {
|
||||
if num, ok = syscallNum[name]; ok {
|
||||
return
|
||||
}
|
||||
num, ok = syscallNumExtra[name]
|
||||
return
|
||||
}
|
||||
54
container/seccomp/syscall_extra_linux_amd64.go
Normal file
54
container/seccomp/syscall_extra_linux_amd64.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package seccomp
|
||||
|
||||
/*
|
||||
#cgo linux pkg-config: --static libseccomp
|
||||
|
||||
#include <seccomp.h>
|
||||
#include <sys/personality.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
const (
|
||||
PER_LINUX = C.PER_LINUX
|
||||
PER_LINUX32 = C.PER_LINUX32
|
||||
)
|
||||
|
||||
var syscallNumExtra = map[string]int{
|
||||
"umount": SYS_UMOUNT,
|
||||
"subpage_prot": SYS_SUBPAGE_PROT,
|
||||
"switch_endian": SYS_SWITCH_ENDIAN,
|
||||
"vm86": SYS_VM86,
|
||||
"vm86old": SYS_VM86OLD,
|
||||
"clock_adjtime64": SYS_CLOCK_ADJTIME64,
|
||||
"clock_settime64": SYS_CLOCK_SETTIME64,
|
||||
"chown32": SYS_CHOWN32,
|
||||
"fchown32": SYS_FCHOWN32,
|
||||
"lchown32": SYS_LCHOWN32,
|
||||
"setgid32": SYS_SETGID32,
|
||||
"setgroups32": SYS_SETGROUPS32,
|
||||
"setregid32": SYS_SETREGID32,
|
||||
"setresgid32": SYS_SETRESGID32,
|
||||
"setresuid32": SYS_SETRESUID32,
|
||||
"setreuid32": SYS_SETREUID32,
|
||||
"setuid32": SYS_SETUID32,
|
||||
}
|
||||
|
||||
const (
|
||||
SYS_UMOUNT = C.__SNR_umount
|
||||
SYS_SUBPAGE_PROT = C.__SNR_subpage_prot
|
||||
SYS_SWITCH_ENDIAN = C.__SNR_switch_endian
|
||||
SYS_VM86 = C.__SNR_vm86
|
||||
SYS_VM86OLD = C.__SNR_vm86old
|
||||
SYS_CLOCK_ADJTIME64 = C.__SNR_clock_adjtime64
|
||||
SYS_CLOCK_SETTIME64 = C.__SNR_clock_settime64
|
||||
SYS_CHOWN32 = C.__SNR_chown32
|
||||
SYS_FCHOWN32 = C.__SNR_fchown32
|
||||
SYS_LCHOWN32 = C.__SNR_lchown32
|
||||
SYS_SETGID32 = C.__SNR_setgid32
|
||||
SYS_SETGROUPS32 = C.__SNR_setgroups32
|
||||
SYS_SETREGID32 = C.__SNR_setregid32
|
||||
SYS_SETRESGID32 = C.__SNR_setresgid32
|
||||
SYS_SETRESUID32 = C.__SNR_setresuid32
|
||||
SYS_SETREUID32 = C.__SNR_setreuid32
|
||||
SYS_SETUID32 = C.__SNR_setuid32
|
||||
)
|
||||
459
container/seccomp/syscall_linux_amd64.go
Normal file
459
container/seccomp/syscall_linux_amd64.go
Normal file
@@ -0,0 +1,459 @@
|
||||
// mksysnum_linux.pl /usr/include/asm/unistd_64.h
|
||||
// Code generated by the command above; DO NOT EDIT.
|
||||
|
||||
package seccomp
|
||||
|
||||
import . "syscall"
|
||||
|
||||
var syscallNum = map[string]int{
|
||||
"read": SYS_READ,
|
||||
"write": SYS_WRITE,
|
||||
"open": SYS_OPEN,
|
||||
"close": SYS_CLOSE,
|
||||
"stat": SYS_STAT,
|
||||
"fstat": SYS_FSTAT,
|
||||
"lstat": SYS_LSTAT,
|
||||
"poll": SYS_POLL,
|
||||
"lseek": SYS_LSEEK,
|
||||
"mmap": SYS_MMAP,
|
||||
"mprotect": SYS_MPROTECT,
|
||||
"munmap": SYS_MUNMAP,
|
||||
"brk": SYS_BRK,
|
||||
"rt_sigaction": SYS_RT_SIGACTION,
|
||||
"rt_sigprocmask": SYS_RT_SIGPROCMASK,
|
||||
"rt_sigreturn": SYS_RT_SIGRETURN,
|
||||
"ioctl": SYS_IOCTL,
|
||||
"pread64": SYS_PREAD64,
|
||||
"pwrite64": SYS_PWRITE64,
|
||||
"readv": SYS_READV,
|
||||
"writev": SYS_WRITEV,
|
||||
"access": SYS_ACCESS,
|
||||
"pipe": SYS_PIPE,
|
||||
"select": SYS_SELECT,
|
||||
"sched_yield": SYS_SCHED_YIELD,
|
||||
"mremap": SYS_MREMAP,
|
||||
"msync": SYS_MSYNC,
|
||||
"mincore": SYS_MINCORE,
|
||||
"madvise": SYS_MADVISE,
|
||||
"shmget": SYS_SHMGET,
|
||||
"shmat": SYS_SHMAT,
|
||||
"shmctl": SYS_SHMCTL,
|
||||
"dup": SYS_DUP,
|
||||
"dup2": SYS_DUP2,
|
||||
"pause": SYS_PAUSE,
|
||||
"nanosleep": SYS_NANOSLEEP,
|
||||
"getitimer": SYS_GETITIMER,
|
||||
"alarm": SYS_ALARM,
|
||||
"setitimer": SYS_SETITIMER,
|
||||
"getpid": SYS_GETPID,
|
||||
"sendfile": SYS_SENDFILE,
|
||||
"socket": SYS_SOCKET,
|
||||
"connect": SYS_CONNECT,
|
||||
"accept": SYS_ACCEPT,
|
||||
"sendto": SYS_SENDTO,
|
||||
"recvfrom": SYS_RECVFROM,
|
||||
"sendmsg": SYS_SENDMSG,
|
||||
"recvmsg": SYS_RECVMSG,
|
||||
"shutdown": SYS_SHUTDOWN,
|
||||
"bind": SYS_BIND,
|
||||
"listen": SYS_LISTEN,
|
||||
"getsockname": SYS_GETSOCKNAME,
|
||||
"getpeername": SYS_GETPEERNAME,
|
||||
"socketpair": SYS_SOCKETPAIR,
|
||||
"setsockopt": SYS_SETSOCKOPT,
|
||||
"getsockopt": SYS_GETSOCKOPT,
|
||||
"clone": SYS_CLONE,
|
||||
"fork": SYS_FORK,
|
||||
"vfork": SYS_VFORK,
|
||||
"execve": SYS_EXECVE,
|
||||
"exit": SYS_EXIT,
|
||||
"wait4": SYS_WAIT4,
|
||||
"kill": SYS_KILL,
|
||||
"uname": SYS_UNAME,
|
||||
"semget": SYS_SEMGET,
|
||||
"semop": SYS_SEMOP,
|
||||
"semctl": SYS_SEMCTL,
|
||||
"shmdt": SYS_SHMDT,
|
||||
"msgget": SYS_MSGGET,
|
||||
"msgsnd": SYS_MSGSND,
|
||||
"msgrcv": SYS_MSGRCV,
|
||||
"msgctl": SYS_MSGCTL,
|
||||
"fcntl": SYS_FCNTL,
|
||||
"flock": SYS_FLOCK,
|
||||
"fsync": SYS_FSYNC,
|
||||
"fdatasync": SYS_FDATASYNC,
|
||||
"truncate": SYS_TRUNCATE,
|
||||
"ftruncate": SYS_FTRUNCATE,
|
||||
"getdents": SYS_GETDENTS,
|
||||
"getcwd": SYS_GETCWD,
|
||||
"chdir": SYS_CHDIR,
|
||||
"fchdir": SYS_FCHDIR,
|
||||
"rename": SYS_RENAME,
|
||||
"mkdir": SYS_MKDIR,
|
||||
"rmdir": SYS_RMDIR,
|
||||
"creat": SYS_CREAT,
|
||||
"link": SYS_LINK,
|
||||
"unlink": SYS_UNLINK,
|
||||
"symlink": SYS_SYMLINK,
|
||||
"readlink": SYS_READLINK,
|
||||
"chmod": SYS_CHMOD,
|
||||
"fchmod": SYS_FCHMOD,
|
||||
"chown": SYS_CHOWN,
|
||||
"fchown": SYS_FCHOWN,
|
||||
"lchown": SYS_LCHOWN,
|
||||
"umask": SYS_UMASK,
|
||||
"gettimeofday": SYS_GETTIMEOFDAY,
|
||||
"getrlimit": SYS_GETRLIMIT,
|
||||
"getrusage": SYS_GETRUSAGE,
|
||||
"sysinfo": SYS_SYSINFO,
|
||||
"times": SYS_TIMES,
|
||||
"ptrace": SYS_PTRACE,
|
||||
"getuid": SYS_GETUID,
|
||||
"syslog": SYS_SYSLOG,
|
||||
"getgid": SYS_GETGID,
|
||||
"setuid": SYS_SETUID,
|
||||
"setgid": SYS_SETGID,
|
||||
"geteuid": SYS_GETEUID,
|
||||
"getegid": SYS_GETEGID,
|
||||
"setpgid": SYS_SETPGID,
|
||||
"getppid": SYS_GETPPID,
|
||||
"getpgrp": SYS_GETPGRP,
|
||||
"setsid": SYS_SETSID,
|
||||
"setreuid": SYS_SETREUID,
|
||||
"setregid": SYS_SETREGID,
|
||||
"getgroups": SYS_GETGROUPS,
|
||||
"setgroups": SYS_SETGROUPS,
|
||||
"setresuid": SYS_SETRESUID,
|
||||
"getresuid": SYS_GETRESUID,
|
||||
"setresgid": SYS_SETRESGID,
|
||||
"getresgid": SYS_GETRESGID,
|
||||
"getpgid": SYS_GETPGID,
|
||||
"setfsuid": SYS_SETFSUID,
|
||||
"setfsgid": SYS_SETFSGID,
|
||||
"getsid": SYS_GETSID,
|
||||
"capget": SYS_CAPGET,
|
||||
"capset": SYS_CAPSET,
|
||||
"rt_sigpending": SYS_RT_SIGPENDING,
|
||||
"rt_sigtimedwait": SYS_RT_SIGTIMEDWAIT,
|
||||
"rt_sigqueueinfo": SYS_RT_SIGQUEUEINFO,
|
||||
"rt_sigsuspend": SYS_RT_SIGSUSPEND,
|
||||
"sigaltstack": SYS_SIGALTSTACK,
|
||||
"utime": SYS_UTIME,
|
||||
"mknod": SYS_MKNOD,
|
||||
"uselib": SYS_USELIB,
|
||||
"personality": SYS_PERSONALITY,
|
||||
"ustat": SYS_USTAT,
|
||||
"statfs": SYS_STATFS,
|
||||
"fstatfs": SYS_FSTATFS,
|
||||
"sysfs": SYS_SYSFS,
|
||||
"getpriority": SYS_GETPRIORITY,
|
||||
"setpriority": SYS_SETPRIORITY,
|
||||
"sched_setparam": SYS_SCHED_SETPARAM,
|
||||
"sched_getparam": SYS_SCHED_GETPARAM,
|
||||
"sched_setscheduler": SYS_SCHED_SETSCHEDULER,
|
||||
"sched_getscheduler": SYS_SCHED_GETSCHEDULER,
|
||||
"sched_get_priority_max": SYS_SCHED_GET_PRIORITY_MAX,
|
||||
"sched_get_priority_min": SYS_SCHED_GET_PRIORITY_MIN,
|
||||
"sched_rr_get_interval": SYS_SCHED_RR_GET_INTERVAL,
|
||||
"mlock": SYS_MLOCK,
|
||||
"munlock": SYS_MUNLOCK,
|
||||
"mlockall": SYS_MLOCKALL,
|
||||
"munlockall": SYS_MUNLOCKALL,
|
||||
"vhangup": SYS_VHANGUP,
|
||||
"modify_ldt": SYS_MODIFY_LDT,
|
||||
"pivot_root": SYS_PIVOT_ROOT,
|
||||
"_sysctl": SYS__SYSCTL,
|
||||
"prctl": SYS_PRCTL,
|
||||
"arch_prctl": SYS_ARCH_PRCTL,
|
||||
"adjtimex": SYS_ADJTIMEX,
|
||||
"setrlimit": SYS_SETRLIMIT,
|
||||
"chroot": SYS_CHROOT,
|
||||
"sync": SYS_SYNC,
|
||||
"acct": SYS_ACCT,
|
||||
"settimeofday": SYS_SETTIMEOFDAY,
|
||||
"mount": SYS_MOUNT,
|
||||
"umount2": SYS_UMOUNT2,
|
||||
"swapon": SYS_SWAPON,
|
||||
"swapoff": SYS_SWAPOFF,
|
||||
"reboot": SYS_REBOOT,
|
||||
"sethostname": SYS_SETHOSTNAME,
|
||||
"setdomainname": SYS_SETDOMAINNAME,
|
||||
"iopl": SYS_IOPL,
|
||||
"ioperm": SYS_IOPERM,
|
||||
"create_module": SYS_CREATE_MODULE,
|
||||
"init_module": SYS_INIT_MODULE,
|
||||
"delete_module": SYS_DELETE_MODULE,
|
||||
"get_kernel_syms": SYS_GET_KERNEL_SYMS,
|
||||
"query_module": SYS_QUERY_MODULE,
|
||||
"quotactl": SYS_QUOTACTL,
|
||||
"nfsservctl": SYS_NFSSERVCTL,
|
||||
"getpmsg": SYS_GETPMSG,
|
||||
"putpmsg": SYS_PUTPMSG,
|
||||
"afs_syscall": SYS_AFS_SYSCALL,
|
||||
"tuxcall": SYS_TUXCALL,
|
||||
"security": SYS_SECURITY,
|
||||
"gettid": SYS_GETTID,
|
||||
"readahead": SYS_READAHEAD,
|
||||
"setxattr": SYS_SETXATTR,
|
||||
"lsetxattr": SYS_LSETXATTR,
|
||||
"fsetxattr": SYS_FSETXATTR,
|
||||
"getxattr": SYS_GETXATTR,
|
||||
"lgetxattr": SYS_LGETXATTR,
|
||||
"fgetxattr": SYS_FGETXATTR,
|
||||
"listxattr": SYS_LISTXATTR,
|
||||
"llistxattr": SYS_LLISTXATTR,
|
||||
"flistxattr": SYS_FLISTXATTR,
|
||||
"removexattr": SYS_REMOVEXATTR,
|
||||
"lremovexattr": SYS_LREMOVEXATTR,
|
||||
"fremovexattr": SYS_FREMOVEXATTR,
|
||||
"tkill": SYS_TKILL,
|
||||
"time": SYS_TIME,
|
||||
"futex": SYS_FUTEX,
|
||||
"sched_setaffinity": SYS_SCHED_SETAFFINITY,
|
||||
"sched_getaffinity": SYS_SCHED_GETAFFINITY,
|
||||
"set_thread_area": SYS_SET_THREAD_AREA,
|
||||
"io_setup": SYS_IO_SETUP,
|
||||
"io_destroy": SYS_IO_DESTROY,
|
||||
"io_getevents": SYS_IO_GETEVENTS,
|
||||
"io_submit": SYS_IO_SUBMIT,
|
||||
"io_cancel": SYS_IO_CANCEL,
|
||||
"get_thread_area": SYS_GET_THREAD_AREA,
|
||||
"lookup_dcookie": SYS_LOOKUP_DCOOKIE,
|
||||
"epoll_create": SYS_EPOLL_CREATE,
|
||||
"epoll_ctl_old": SYS_EPOLL_CTL_OLD,
|
||||
"epoll_wait_old": SYS_EPOLL_WAIT_OLD,
|
||||
"remap_file_pages": SYS_REMAP_FILE_PAGES,
|
||||
"getdents64": SYS_GETDENTS64,
|
||||
"set_tid_address": SYS_SET_TID_ADDRESS,
|
||||
"restart_syscall": SYS_RESTART_SYSCALL,
|
||||
"semtimedop": SYS_SEMTIMEDOP,
|
||||
"fadvise64": SYS_FADVISE64,
|
||||
"timer_create": SYS_TIMER_CREATE,
|
||||
"timer_settime": SYS_TIMER_SETTIME,
|
||||
"timer_gettime": SYS_TIMER_GETTIME,
|
||||
"timer_getoverrun": SYS_TIMER_GETOVERRUN,
|
||||
"timer_delete": SYS_TIMER_DELETE,
|
||||
"clock_settime": SYS_CLOCK_SETTIME,
|
||||
"clock_gettime": SYS_CLOCK_GETTIME,
|
||||
"clock_getres": SYS_CLOCK_GETRES,
|
||||
"clock_nanosleep": SYS_CLOCK_NANOSLEEP,
|
||||
"exit_group": SYS_EXIT_GROUP,
|
||||
"epoll_wait": SYS_EPOLL_WAIT,
|
||||
"epoll_ctl": SYS_EPOLL_CTL,
|
||||
"tgkill": SYS_TGKILL,
|
||||
"utimes": SYS_UTIMES,
|
||||
"vserver": SYS_VSERVER,
|
||||
"mbind": SYS_MBIND,
|
||||
"set_mempolicy": SYS_SET_MEMPOLICY,
|
||||
"get_mempolicy": SYS_GET_MEMPOLICY,
|
||||
"mq_open": SYS_MQ_OPEN,
|
||||
"mq_unlink": SYS_MQ_UNLINK,
|
||||
"mq_timedsend": SYS_MQ_TIMEDSEND,
|
||||
"mq_timedreceive": SYS_MQ_TIMEDRECEIVE,
|
||||
"mq_notify": SYS_MQ_NOTIFY,
|
||||
"mq_getsetattr": SYS_MQ_GETSETATTR,
|
||||
"kexec_load": SYS_KEXEC_LOAD,
|
||||
"waitid": SYS_WAITID,
|
||||
"add_key": SYS_ADD_KEY,
|
||||
"request_key": SYS_REQUEST_KEY,
|
||||
"keyctl": SYS_KEYCTL,
|
||||
"ioprio_set": SYS_IOPRIO_SET,
|
||||
"ioprio_get": SYS_IOPRIO_GET,
|
||||
"inotify_init": SYS_INOTIFY_INIT,
|
||||
"inotify_add_watch": SYS_INOTIFY_ADD_WATCH,
|
||||
"inotify_rm_watch": SYS_INOTIFY_RM_WATCH,
|
||||
"migrate_pages": SYS_MIGRATE_PAGES,
|
||||
"openat": SYS_OPENAT,
|
||||
"mkdirat": SYS_MKDIRAT,
|
||||
"mknodat": SYS_MKNODAT,
|
||||
"fchownat": SYS_FCHOWNAT,
|
||||
"futimesat": SYS_FUTIMESAT,
|
||||
"newfstatat": SYS_NEWFSTATAT,
|
||||
"unlinkat": SYS_UNLINKAT,
|
||||
"renameat": SYS_RENAMEAT,
|
||||
"linkat": SYS_LINKAT,
|
||||
"symlinkat": SYS_SYMLINKAT,
|
||||
"readlinkat": SYS_READLINKAT,
|
||||
"fchmodat": SYS_FCHMODAT,
|
||||
"faccessat": SYS_FACCESSAT,
|
||||
"pselect6": SYS_PSELECT6,
|
||||
"ppoll": SYS_PPOLL,
|
||||
"unshare": SYS_UNSHARE,
|
||||
"set_robust_list": SYS_SET_ROBUST_LIST,
|
||||
"get_robust_list": SYS_GET_ROBUST_LIST,
|
||||
"splice": SYS_SPLICE,
|
||||
"tee": SYS_TEE,
|
||||
"sync_file_range": SYS_SYNC_FILE_RANGE,
|
||||
"vmsplice": SYS_VMSPLICE,
|
||||
"move_pages": SYS_MOVE_PAGES,
|
||||
"utimensat": SYS_UTIMENSAT,
|
||||
"epoll_pwait": SYS_EPOLL_PWAIT,
|
||||
"signalfd": SYS_SIGNALFD,
|
||||
"timerfd_create": SYS_TIMERFD_CREATE,
|
||||
"eventfd": SYS_EVENTFD,
|
||||
"fallocate": SYS_FALLOCATE,
|
||||
"timerfd_settime": SYS_TIMERFD_SETTIME,
|
||||
"timerfd_gettime": SYS_TIMERFD_GETTIME,
|
||||
"accept4": SYS_ACCEPT4,
|
||||
"signalfd4": SYS_SIGNALFD4,
|
||||
"eventfd2": SYS_EVENTFD2,
|
||||
"epoll_create1": SYS_EPOLL_CREATE1,
|
||||
"dup3": SYS_DUP3,
|
||||
"pipe2": SYS_PIPE2,
|
||||
"inotify_init1": SYS_INOTIFY_INIT1,
|
||||
"preadv": SYS_PREADV,
|
||||
"pwritev": SYS_PWRITEV,
|
||||
"rt_tgsigqueueinfo": SYS_RT_TGSIGQUEUEINFO,
|
||||
"perf_event_open": SYS_PERF_EVENT_OPEN,
|
||||
"recvmmsg": SYS_RECVMMSG,
|
||||
"fanotify_init": SYS_FANOTIFY_INIT,
|
||||
"fanotify_mark": SYS_FANOTIFY_MARK,
|
||||
"prlimit64": SYS_PRLIMIT64,
|
||||
"name_to_handle_at": SYS_NAME_TO_HANDLE_AT,
|
||||
"open_by_handle_at": SYS_OPEN_BY_HANDLE_AT,
|
||||
"clock_adjtime": SYS_CLOCK_ADJTIME,
|
||||
"syncfs": SYS_SYNCFS,
|
||||
"sendmmsg": SYS_SENDMMSG,
|
||||
"setns": SYS_SETNS,
|
||||
"getcpu": SYS_GETCPU,
|
||||
"process_vm_readv": SYS_PROCESS_VM_READV,
|
||||
"process_vm_writev": SYS_PROCESS_VM_WRITEV,
|
||||
"kcmp": SYS_KCMP,
|
||||
"finit_module": SYS_FINIT_MODULE,
|
||||
"sched_setattr": SYS_SCHED_SETATTR,
|
||||
"sched_getattr": SYS_SCHED_GETATTR,
|
||||
"renameat2": SYS_RENAMEAT2,
|
||||
"seccomp": SYS_SECCOMP,
|
||||
"getrandom": SYS_GETRANDOM,
|
||||
"memfd_create": SYS_MEMFD_CREATE,
|
||||
"kexec_file_load": SYS_KEXEC_FILE_LOAD,
|
||||
"bpf": SYS_BPF,
|
||||
"execveat": SYS_EXECVEAT,
|
||||
"userfaultfd": SYS_USERFAULTFD,
|
||||
"membarrier": SYS_MEMBARRIER,
|
||||
"mlock2": SYS_MLOCK2,
|
||||
"copy_file_range": SYS_COPY_FILE_RANGE,
|
||||
"preadv2": SYS_PREADV2,
|
||||
"pwritev2": SYS_PWRITEV2,
|
||||
"pkey_mprotect": SYS_PKEY_MPROTECT,
|
||||
"pkey_alloc": SYS_PKEY_ALLOC,
|
||||
"pkey_free": SYS_PKEY_FREE,
|
||||
"statx": SYS_STATX,
|
||||
"io_pgetevents": SYS_IO_PGETEVENTS,
|
||||
"rseq": SYS_RSEQ,
|
||||
"uretprobe": SYS_URETPROBE,
|
||||
"pidfd_send_signal": SYS_PIDFD_SEND_SIGNAL,
|
||||
"io_uring_setup": SYS_IO_URING_SETUP,
|
||||
"io_uring_enter": SYS_IO_URING_ENTER,
|
||||
"io_uring_register": SYS_IO_URING_REGISTER,
|
||||
"open_tree": SYS_OPEN_TREE,
|
||||
"move_mount": SYS_MOVE_MOUNT,
|
||||
"fsopen": SYS_FSOPEN,
|
||||
"fsconfig": SYS_FSCONFIG,
|
||||
"fsmount": SYS_FSMOUNT,
|
||||
"fspick": SYS_FSPICK,
|
||||
"pidfd_open": SYS_PIDFD_OPEN,
|
||||
"clone3": SYS_CLONE3,
|
||||
"close_range": SYS_CLOSE_RANGE,
|
||||
"openat2": SYS_OPENAT2,
|
||||
"pidfd_getfd": SYS_PIDFD_GETFD,
|
||||
"faccessat2": SYS_FACCESSAT2,
|
||||
"process_madvise": SYS_PROCESS_MADVISE,
|
||||
"epoll_pwait2": SYS_EPOLL_PWAIT2,
|
||||
"mount_setattr": SYS_MOUNT_SETATTR,
|
||||
"quotactl_fd": SYS_QUOTACTL_FD,
|
||||
"landlock_create_ruleset": SYS_LANDLOCK_CREATE_RULESET,
|
||||
"landlock_add_rule": SYS_LANDLOCK_ADD_RULE,
|
||||
"landlock_restrict_self": SYS_LANDLOCK_RESTRICT_SELF,
|
||||
"memfd_secret": SYS_MEMFD_SECRET,
|
||||
"process_mrelease": SYS_PROCESS_MRELEASE,
|
||||
"futex_waitv": SYS_FUTEX_WAITV,
|
||||
"set_mempolicy_home_node": SYS_SET_MEMPOLICY_HOME_NODE,
|
||||
"cachestat": SYS_CACHESTAT,
|
||||
"fchmodat2": SYS_FCHMODAT2,
|
||||
"map_shadow_stack": SYS_MAP_SHADOW_STACK,
|
||||
"futex_wake": SYS_FUTEX_WAKE,
|
||||
"futex_wait": SYS_FUTEX_WAIT,
|
||||
"futex_requeue": SYS_FUTEX_REQUEUE,
|
||||
"statmount": SYS_STATMOUNT,
|
||||
"listmount": SYS_LISTMOUNT,
|
||||
"lsm_get_self_attr": SYS_LSM_GET_SELF_ATTR,
|
||||
"lsm_set_self_attr": SYS_LSM_SET_SELF_ATTR,
|
||||
"lsm_list_modules": SYS_LSM_LIST_MODULES,
|
||||
"mseal": SYS_MSEAL,
|
||||
}
|
||||
|
||||
const (
|
||||
SYS_NAME_TO_HANDLE_AT = 303
|
||||
SYS_OPEN_BY_HANDLE_AT = 304
|
||||
SYS_CLOCK_ADJTIME = 305
|
||||
SYS_SYNCFS = 306
|
||||
SYS_SENDMMSG = 307
|
||||
SYS_SETNS = 308
|
||||
SYS_GETCPU = 309
|
||||
SYS_PROCESS_VM_READV = 310
|
||||
SYS_PROCESS_VM_WRITEV = 311
|
||||
SYS_KCMP = 312
|
||||
SYS_FINIT_MODULE = 313
|
||||
SYS_SCHED_SETATTR = 314
|
||||
SYS_SCHED_GETATTR = 315
|
||||
SYS_RENAMEAT2 = 316
|
||||
SYS_SECCOMP = 317
|
||||
SYS_GETRANDOM = 318
|
||||
SYS_MEMFD_CREATE = 319
|
||||
SYS_KEXEC_FILE_LOAD = 320
|
||||
SYS_BPF = 321
|
||||
SYS_EXECVEAT = 322
|
||||
SYS_USERFAULTFD = 323
|
||||
SYS_MEMBARRIER = 324
|
||||
SYS_MLOCK2 = 325
|
||||
SYS_COPY_FILE_RANGE = 326
|
||||
SYS_PREADV2 = 327
|
||||
SYS_PWRITEV2 = 328
|
||||
SYS_PKEY_MPROTECT = 329
|
||||
SYS_PKEY_ALLOC = 330
|
||||
SYS_PKEY_FREE = 331
|
||||
SYS_STATX = 332
|
||||
SYS_IO_PGETEVENTS = 333
|
||||
SYS_RSEQ = 334
|
||||
SYS_URETPROBE = 335
|
||||
SYS_PIDFD_SEND_SIGNAL = 424
|
||||
SYS_IO_URING_SETUP = 425
|
||||
SYS_IO_URING_ENTER = 426
|
||||
SYS_IO_URING_REGISTER = 427
|
||||
SYS_OPEN_TREE = 428
|
||||
SYS_MOVE_MOUNT = 429
|
||||
SYS_FSOPEN = 430
|
||||
SYS_FSCONFIG = 431
|
||||
SYS_FSMOUNT = 432
|
||||
SYS_FSPICK = 433
|
||||
SYS_PIDFD_OPEN = 434
|
||||
SYS_CLONE3 = 435
|
||||
SYS_CLOSE_RANGE = 436
|
||||
SYS_OPENAT2 = 437
|
||||
SYS_PIDFD_GETFD = 438
|
||||
SYS_FACCESSAT2 = 439
|
||||
SYS_PROCESS_MADVISE = 440
|
||||
SYS_EPOLL_PWAIT2 = 441
|
||||
SYS_MOUNT_SETATTR = 442
|
||||
SYS_QUOTACTL_FD = 443
|
||||
SYS_LANDLOCK_CREATE_RULESET = 444
|
||||
SYS_LANDLOCK_ADD_RULE = 445
|
||||
SYS_LANDLOCK_RESTRICT_SELF = 446
|
||||
SYS_MEMFD_SECRET = 447
|
||||
SYS_PROCESS_MRELEASE = 448
|
||||
SYS_FUTEX_WAITV = 449
|
||||
SYS_SET_MEMPOLICY_HOME_NODE = 450
|
||||
SYS_CACHESTAT = 451
|
||||
SYS_FCHMODAT2 = 452
|
||||
SYS_MAP_SHADOW_STACK = 453
|
||||
SYS_FUTEX_WAKE = 454
|
||||
SYS_FUTEX_WAIT = 455
|
||||
SYS_FUTEX_REQUEUE = 456
|
||||
SYS_STATMOUNT = 457
|
||||
SYS_LISTMOUNT = 458
|
||||
SYS_LSM_GET_SELF_ATTR = 459
|
||||
SYS_LSM_SET_SELF_ATTR = 460
|
||||
SYS_LSM_LIST_MODULES = 461
|
||||
SYS_MSEAL = 462
|
||||
)
|
||||
20
container/seccomp/syscall_test.go
Normal file
20
container/seccomp/syscall_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package seccomp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSyscallResolveName(t *testing.T) {
|
||||
for name, want := range Syscalls() {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if got := syscallResolveName(name); got != want {
|
||||
t.Errorf("syscallResolveName(%q) = %d, want %d",
|
||||
name, got, want)
|
||||
}
|
||||
if got, ok := SyscallResolveName(name); !ok || got != want {
|
||||
t.Errorf("SyscallResolveName(%q) = %d, want %d",
|
||||
name, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package sandbox
|
||||
package container
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
@@ -1,4 +1,4 @@
|
||||
package sandbox
|
||||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -3,7 +3,7 @@ package vfs_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/sandbox/vfs"
|
||||
"hakurei.app/container/vfs"
|
||||
)
|
||||
|
||||
func TestUnmangle(t *testing.T) {
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/sandbox/vfs"
|
||||
"hakurei.app/container/vfs"
|
||||
)
|
||||
|
||||
func TestMountInfo(t *testing.T) {
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/sandbox/vfs"
|
||||
"hakurei.app/container/vfs"
|
||||
)
|
||||
|
||||
func TestUnfold(t *testing.T) {
|
||||
@@ -1,251 +0,0 @@
|
||||
package dbus_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/dbus"
|
||||
"git.gensokyo.uk/security/fortify/helper"
|
||||
"git.gensokyo.uk/security/fortify/internal"
|
||||
"git.gensokyo.uk/security/fortify/internal/fmsg"
|
||||
"git.gensokyo.uk/security/fortify/sandbox"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
for _, tc := range [][2][2]string{
|
||||
{
|
||||
{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/1ca5d183ef4c99e74c3e544715f32702/bus"},
|
||||
{"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() {
|
||||
t.Run("create seal for "+id, func(t *testing.T) {
|
||||
p := dbus.New(tc[0].bus, tc[1].bus)
|
||||
if err := p.Seal(tc[0].c, tc[1].c); (errors.Is(err, syscall.EINVAL)) != tc[0].wantErr {
|
||||
t.Errorf("Seal(%p, %p) error = %v, wantErr %v",
|
||||
tc[0].c, tc[1].c,
|
||||
err, tc[0].wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
// rest of the tests happen for sealed instances
|
||||
if tc[0].wantErr {
|
||||
return
|
||||
}
|
||||
|
||||
// build null-terminated string from wanted args
|
||||
want := new(strings.Builder)
|
||||
args := append(tc[0].want, tc[1].want...)
|
||||
for _, arg := range args {
|
||||
want.WriteString(arg)
|
||||
want.WriteByte('\x00')
|
||||
}
|
||||
|
||||
wt := p.AccessTestProxySeal()
|
||||
got := new(strings.Builder)
|
||||
if _, err := wt.WriteTo(got); err != nil {
|
||||
t.Errorf("p.seal.WriteTo(): %v", err)
|
||||
}
|
||||
|
||||
if want.String() != got.String() {
|
||||
t.Errorf("Seal(%p, %p) seal = %v, want %v",
|
||||
tc[0].c, tc[1].c,
|
||||
got.String(), want.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxy_Start_Wait_Close_String(t *testing.T) {
|
||||
oldWaitDelay := helper.WaitDelay
|
||||
helper.WaitDelay = 16 * time.Second
|
||||
t.Cleanup(func() { helper.WaitDelay = oldWaitDelay })
|
||||
|
||||
t.Run("sandbox", func(t *testing.T) {
|
||||
proxyName := dbus.ProxyName
|
||||
dbus.ProxyName = os.Args[0]
|
||||
t.Cleanup(func() { dbus.ProxyName = proxyName })
|
||||
testProxyStartWaitCloseString(t, true)
|
||||
})
|
||||
t.Run("direct", func(t *testing.T) { testProxyStartWaitCloseString(t, false) })
|
||||
}
|
||||
|
||||
func testProxyStartWaitCloseString(t *testing.T, useSandbox bool) {
|
||||
for id, tc := range testCasePairs() {
|
||||
// this test does not test errors
|
||||
if tc[0].wantErr {
|
||||
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) {
|
||||
p := dbus.New(tc[0].bus, tc[1].bus)
|
||||
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("unsealed", func(t *testing.T) {
|
||||
t.Run("string", 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("start", func(t *testing.T) {
|
||||
want := "proxy not sealed"
|
||||
if err := p.Start(context.Background(), nil, useSandbox); err == nil || err.Error() != want {
|
||||
t.Errorf("Start() error = %v, wantErr %q",
|
||||
err, errors.New(want))
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("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("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)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sealed", func(t *testing.T) {
|
||||
want := strings.Join(append(tc[0].want, tc[1].want...), " ")
|
||||
if got := p.String(); got != want {
|
||||
t.Errorf("String() = %v, want %v",
|
||||
got, want)
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("start", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := p.Start(ctx, output, useSandbox); err != nil {
|
||||
t.Fatalf("Start(nil, nil) error = %v",
|
||||
err)
|
||||
}
|
||||
|
||||
t.Run("string", func(t *testing.T) {
|
||||
wantSubstr := fmt.Sprintf("%s -test.run=TestHelperStub -- --args=3 --fd=4", os.Args[0])
|
||||
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() = %v, want %v",
|
||||
p.String(), wantSubstr)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wait", func(t *testing.T) {
|
||||
p.Close()
|
||||
if err := p.Wait(); err != nil {
|
||||
t.Errorf("Wait() error = %v\noutput: %s",
|
||||
err, output.String())
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelperInit(t *testing.T) {
|
||||
if len(os.Args) != 5 || os.Args[4] != "init" {
|
||||
return
|
||||
}
|
||||
sandbox.SetOutput(fmsg.Output{})
|
||||
sandbox.Init(fmsg.Prepare, internal.InstallFmsg)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package dbus
|
||||
|
||||
import "io"
|
||||
|
||||
// CompareTestNew provides TestNew with comparison access to unexported Proxy fields.
|
||||
func (p *Proxy) CompareTestNew(session, system [2]string) bool {
|
||||
return session == p.session && system == p.system
|
||||
}
|
||||
|
||||
// AccessTestProxySeal provides TestProxy_Seal with access to unexported Proxy seal field.
|
||||
func (p *Proxy) AccessTestProxySeal() io.WriterTo {
|
||||
return p.seal
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package dbus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"sync"
|
||||
|
||||
"git.gensokyo.uk/security/fortify/helper"
|
||||
)
|
||||
|
||||
// ProxyName is the file name or path to the proxy program.
|
||||
// Overriding ProxyName will only affect Proxy instance created after the change.
|
||||
var ProxyName = "xdg-dbus-proxy"
|
||||
|
||||
// Proxy holds references to a xdg-dbus-proxy process, and should never be copied.
|
||||
// Once sealed, configuration changes will no longer be possible and attempting to do so will result in a panic.
|
||||
type Proxy struct {
|
||||
helper helper.Helper
|
||||
ctx context.Context
|
||||
cancel context.CancelCauseFunc
|
||||
|
||||
name string
|
||||
session [2]string
|
||||
system [2]string
|
||||
CmdF func(any)
|
||||
sysP bool
|
||||
|
||||
CommandContext func(ctx context.Context) (cmd *exec.Cmd)
|
||||
FilterF func([]byte) []byte
|
||||
|
||||
seal io.WriterTo
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func (p *Proxy) Session() [2]string { return p.session }
|
||||
func (p *Proxy) System() [2]string { return p.system }
|
||||
func (p *Proxy) Sealed() bool { p.lock.RLock(); defer p.lock.RUnlock(); return p.seal != nil }
|
||||
|
||||
var (
|
||||
ErrConfig = errors.New("no configuration to seal")
|
||||
)
|
||||
|
||||
func (p *Proxy) String() string {
|
||||
if p == nil {
|
||||
return "(invalid dbus proxy)"
|
||||
}
|
||||
|
||||
p.lock.RLock()
|
||||
defer p.lock.RUnlock()
|
||||
|
||||
if p.helper != nil {
|
||||
return p.helper.String()
|
||||
}
|
||||
|
||||
if p.seal != nil {
|
||||
return p.seal.(fmt.Stringer).String()
|
||||
}
|
||||
|
||||
return "(unsealed dbus proxy)"
|
||||
}
|
||||
|
||||
// Seal seals the Proxy instance.
|
||||
func (p *Proxy) Seal(session, system *Config) error {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
if p.seal != nil {
|
||||
panic("dbus proxy sealed twice")
|
||||
}
|
||||
|
||||
if session == nil && system == nil {
|
||||
return ErrConfig
|
||||
}
|
||||
|
||||
var args []string
|
||||
if session != nil {
|
||||
args = append(args, session.Args(p.session)...)
|
||||
}
|
||||
if system != nil {
|
||||
args = append(args, system.Args(p.system)...)
|
||||
p.sysP = true
|
||||
}
|
||||
if seal, err := helper.NewCheckedArgs(args); err != nil {
|
||||
return err
|
||||
} else {
|
||||
p.seal = seal
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// New returns a reference to a new unsealed Proxy.
|
||||
func New(session, system [2]string) *Proxy {
|
||||
return &Proxy{name: ProxyName, session: session, system: system}
|
||||
}
|
||||
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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user