Compare commits
36 Commits
v0.4.2
..
210d36242e
| Author | SHA1 | Date | |
|---|---|---|---|
|
210d36242e
|
|||
|
ea10c4d5a5
|
|||
|
9c6439d7be
|
|||
|
11bd0797f5
|
|||
|
25db474cf6
|
|||
|
21f50aa2f0
|
|||
|
031b0ed270
|
|||
|
a837557505
|
|||
|
aab806a6f9
|
|||
|
3acf49f979
|
|||
|
3057964cc9
|
|||
|
a5ce8dd979
|
|||
|
fe51d5f78c
|
|||
|
1be5a34569
|
|||
|
57ade7aeaa
|
|||
|
d13dfeca22
|
|||
|
56c5a7399b
|
|||
|
e1f07be0b2
|
|||
|
062bb57b27
|
|||
|
03355fb209
|
|||
|
289dea3f85
|
|||
|
2a9e404c30
|
|||
|
85a757101e
|
|||
|
358e94d3c1
|
|||
|
202efb7c7c
|
|||
|
d137a70621
|
|||
|
5b3932ab65
|
|||
|
ea8a7a341f
|
|||
|
5b6546b541
|
|||
|
f60b854ea1
|
|||
|
58999599d8
|
|||
|
08937cf3a0
|
|||
|
68c50b3ebc
|
|||
|
49cc97f67c
|
|||
|
56d2029316
|
|||
|
22b33e0375
|
@@ -1,2 +0,0 @@
|
|||||||
ColumnLimit: 0
|
|
||||||
IndentWidth: 4
|
|
||||||
@@ -2,6 +2,7 @@ name: Test
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
- push
|
- push
|
||||||
|
- pull_request
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
hakurei:
|
hakurei:
|
||||||
@@ -72,20 +73,20 @@ jobs:
|
|||||||
path: result/*
|
path: result/*
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
sharefs:
|
hpkg:
|
||||||
name: ShareFS
|
name: Hpkg
|
||||||
runs-on: nix
|
runs-on: nix
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Run NixOS test
|
- name: Run NixOS test
|
||||||
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sharefs
|
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.hpkg
|
||||||
|
|
||||||
- name: Upload test output
|
- name: Upload test output
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: "sharefs-vm-output"
|
name: "hpkg-vm-output"
|
||||||
path: result/*
|
path: result/*
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
@@ -96,7 +97,7 @@ jobs:
|
|||||||
- race
|
- race
|
||||||
- sandbox
|
- sandbox
|
||||||
- sandbox-race
|
- sandbox-race
|
||||||
- sharefs
|
- hpkg
|
||||||
runs-on: nix
|
runs-on: nix
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
DO NOT ADD NEW ACTIONS HERE
|
||||||
|
|
||||||
|
This port is solely for releasing to the github mirror and serves no purpose during development.
|
||||||
|
All development happens at https://git.gensokyo.uk/security/hakurei. If you wish to contribute,
|
||||||
|
request for an account on git.gensokyo.uk.
|
||||||
@@ -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-**
|
||||||
@@ -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
|
||||||
+27
-7
@@ -1,15 +1,35 @@
|
|||||||
# produced by tools and text editors
|
# Binaries for programs and plugins
|
||||||
*.qcow2
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.pkg
|
||||||
|
/hakurei
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
*.out
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# env file
|
||||||
|
.env
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
# go generate
|
# go generate
|
||||||
/cmd/hakurei/LICENSE
|
/cmd/hakurei/LICENSE
|
||||||
/cmd/mbf/internal/pkgserver/ui/static
|
|
||||||
/internal/pkg/internal/testtool/testtool
|
|
||||||
/internal/rosa/hakurei_current.tar.gz
|
|
||||||
|
|
||||||
# cmd/dist default destination
|
# release
|
||||||
/dist
|
/dist/hakurei-*
|
||||||
|
|
||||||
|
# interactive nixos vm
|
||||||
|
nixos.qcow2
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://git.gensokyo.uk/rosa/hakurei">
|
<a href="https://git.gensokyo.uk/security/hakurei">
|
||||||
<picture>
|
<picture>
|
||||||
<img src="https://basement.gensokyo.uk/images/yukari1.png" width="200px" alt="Yukari">
|
<img src="https://basement.gensokyo.uk/images/yukari1.png" width="200px" alt="Yukari">
|
||||||
</picture>
|
</picture>
|
||||||
@@ -8,58 +8,171 @@
|
|||||||
|
|
||||||
<p align="center">
|
<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://pkg.go.dev/hakurei.app"><img src="https://pkg.go.dev/badge/hakurei.app.svg" alt="Go Reference" /></a>
|
||||||
<a href="https://git.gensokyo.uk/rosa/hakurei/actions"><img src="https://git.gensokyo.uk/rosa/hakurei/actions/workflows/test.yml/badge.svg?branch=staging&style=flat-square" alt="Gitea Workflow Status" /></a>
|
<a href="https://git.gensokyo.uk/security/hakurei/actions"><img src="https://git.gensokyo.uk/security/hakurei/actions/workflows/test.yml/badge.svg?branch=staging&style=flat-square" alt="Gitea Workflow Status" /></a>
|
||||||
<br/>
|
<br/>
|
||||||
<a href="https://git.gensokyo.uk/rosa/hakurei/releases"><img src="https://img.shields.io/gitea/v/release/rosa/hakurei?gitea_url=https%3A%2F%2Fgit.gensokyo.uk&color=purple" alt="Release" /></a>
|
<a href="https://git.gensokyo.uk/security/hakurei/releases"><img src="https://img.shields.io/gitea/v/release/security/hakurei?gitea_url=https%3A%2F%2Fgit.gensokyo.uk&color=purple" alt="Release" /></a>
|
||||||
<a href="https://goreportcard.com/report/hakurei.app"><img src="https://goreportcard.com/badge/hakurei.app" alt="Go Report Card" /></a>
|
<a href="https://goreportcard.com/report/hakurei.app"><img src="https://goreportcard.com/badge/hakurei.app" alt="Go Report Card" /></a>
|
||||||
<a href="https://hakurei.app"><img src="https://img.shields.io/website?url=https%3A%2F%2Fhakurei.app" alt="Website" /></a>
|
<a href="https://hakurei.app"><img src="https://img.shields.io/website?url=https%3A%2F%2Fhakurei.app" alt="Website" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Hakurei is a tool for running sandboxed desktop applications as dedicated
|
Hakurei is a tool for running sandboxed graphical applications as dedicated subordinate users on the Linux kernel.
|
||||||
subordinate users on the Linux kernel. It implements the application container
|
It implements the application container of [planterette (WIP)](https://git.gensokyo.uk/security/planterette),
|
||||||
of [planterette (WIP)](https://git.gensokyo.uk/rosa/planterette), a
|
a self-contained Android-like package manager with modern security features.
|
||||||
self-contained Android-like package manager with modern security features.
|
|
||||||
|
|
||||||
Interaction with hakurei happens entirely through structures described by
|
## NixOS Module usage
|
||||||
package [hst](https://pkg.go.dev/hakurei.app/hst). No native API is available
|
|
||||||
due to internal details of uid isolation.
|
|
||||||
|
|
||||||
## Notable Packages
|
The NixOS module currently requires home-manager to configure subordinate users. Full module documentation can be found [here](options.md).
|
||||||
|
|
||||||
Package [container](https://pkg.go.dev/hakurei.app/container) is general purpose
|
To use the module, import it into your configuration with
|
||||||
container tooling. It is used by the hakurei shim process running as the target
|
|
||||||
subordinate user to set up the application container. It has a single dependency,
|
|
||||||
[libseccomp](https://github.com/seccomp/libseccomp), to create BPF programs
|
|
||||||
for the [system call filter](https://www.kernel.org/doc/html/latest/userspace-api/seccomp_filter.html).
|
|
||||||
|
|
||||||
Package [internal/pkg](https://pkg.go.dev/hakurei.app/internal/pkg) provides
|
```nix
|
||||||
infrastructure for hermetic builds. This replaces the legacy nix-based testing
|
{
|
||||||
framework and serves as the build system of Rosa OS, currently developed under
|
inputs = {
|
||||||
package [internal/rosa](https://pkg.go.dev/hakurei.app/internal/rosa).
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
|
||||||
|
|
||||||
## Dependencies
|
hakurei = {
|
||||||
|
url = "git+https://git.gensokyo.uk/security/hakurei";
|
||||||
|
|
||||||
`container` depends on:
|
# Optional but recommended to limit the size of your system closure.
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
- [libseccomp](https://github.com/seccomp/libseccomp) to generate BPF programs.
|
outputs = { self, nixpkgs, hakurei, ... }:
|
||||||
|
{
|
||||||
|
nixosConfigurations.hakurei = nixpkgs.lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
modules = [
|
||||||
|
hakurei.nixosModules.hakurei
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
`cmd/hakurei` depends on:
|
This adds the `environment.hakurei` option:
|
||||||
|
|
||||||
- [acl](https://savannah.nongnu.org/projects/acl/) to export sockets to
|
```nix
|
||||||
subordinate users.
|
{ pkgs, ... }:
|
||||||
- [wayland](https://gitlab.freedesktop.org/wayland/wayland) to set up
|
|
||||||
[security-context-v1](https://wayland.app/protocols/security-context-v1).
|
|
||||||
- [xcb](https://xcb.freedesktop.org/) to grant and revoke subordinate users
|
|
||||||
access to the X server.
|
|
||||||
|
|
||||||
`cmd/sharefs` depends on:
|
{
|
||||||
|
environment.hakurei = {
|
||||||
|
enable = true;
|
||||||
|
stateDir = "/var/lib/hakurei";
|
||||||
|
users = {
|
||||||
|
alice = 0;
|
||||||
|
nixos = 10;
|
||||||
|
};
|
||||||
|
|
||||||
- [fuse](https://github.com/libfuse/libfuse) to implement the filesystem.
|
commonPaths = [
|
||||||
|
{
|
||||||
|
src = "/sdcard";
|
||||||
|
write = true;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
New dependencies will generally not be added. Patches adding new dependencies
|
extraHomeConfig = {
|
||||||
are very likely to be rejected.
|
home.stateVersion = "23.05";
|
||||||
|
};
|
||||||
|
|
||||||
## NixOS Module (deprecated)
|
apps = {
|
||||||
|
"org.chromium.Chromium" = {
|
||||||
|
name = "chromium";
|
||||||
|
identity = 1;
|
||||||
|
packages = [ pkgs.chromium ];
|
||||||
|
userns = true;
|
||||||
|
mapRealUid = true;
|
||||||
|
dbus = {
|
||||||
|
system = {
|
||||||
|
filter = true;
|
||||||
|
talk = [
|
||||||
|
"org.bluez"
|
||||||
|
"org.freedesktop.Avahi"
|
||||||
|
"org.freedesktop.UPower"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
session =
|
||||||
|
f:
|
||||||
|
f {
|
||||||
|
talk = [
|
||||||
|
"org.freedesktop.FileManager1"
|
||||||
|
"org.freedesktop.Notifications"
|
||||||
|
"org.freedesktop.ScreenSaver"
|
||||||
|
"org.freedesktop.secrets"
|
||||||
|
"org.kde.kwalletd5"
|
||||||
|
"org.kde.kwalletd6"
|
||||||
|
];
|
||||||
|
own = [
|
||||||
|
"org.chromium.Chromium.*"
|
||||||
|
"org.mpris.MediaPlayer2.org.chromium.Chromium.*"
|
||||||
|
"org.mpris.MediaPlayer2.chromium.*"
|
||||||
|
];
|
||||||
|
call = { };
|
||||||
|
broadcast = { };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
The NixOS module is in maintenance mode and will be removed once planterette is
|
"org.claws_mail.Claws-Mail" = {
|
||||||
feature-complete. Full module documentation can be found [here](options.md).
|
name = "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;
|
||||||
|
x11 = false;
|
||||||
|
dbus = true;
|
||||||
|
pulse = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
"dev.vencord.Vesktop" = {
|
||||||
|
name = "discord";
|
||||||
|
identity = 3;
|
||||||
|
shareUid = true;
|
||||||
|
packages = [ pkgs.vesktop ];
|
||||||
|
share = pkgs.vesktop;
|
||||||
|
command = "vesktop --ozone-platform-hint=wayland";
|
||||||
|
userns = true;
|
||||||
|
mapRealUid = true;
|
||||||
|
capability.x11 = true;
|
||||||
|
dbus = {
|
||||||
|
session =
|
||||||
|
f:
|
||||||
|
f {
|
||||||
|
talk = [ "org.kde.StatusNotifierWatcher" ];
|
||||||
|
own = [ ];
|
||||||
|
call = { };
|
||||||
|
broadcast = { };
|
||||||
|
};
|
||||||
|
system.filter = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
"io.looking-glass" = {
|
||||||
|
name = "looking-glass-client";
|
||||||
|
identity = 4;
|
||||||
|
useCommonPaths = false;
|
||||||
|
groups = [ "plugdev" ];
|
||||||
|
extraPaths = [
|
||||||
|
{
|
||||||
|
src = "/dev/shm/looking-glass";
|
||||||
|
write = true;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
extraConfig = {
|
||||||
|
programs.looking-glass-client.enable = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/sh -e
|
|
||||||
|
|
||||||
HAKUREI_DIST_MAKE='' exec "$(dirname -- "$0")/cmd/dist/dist.sh"
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
// Package check provides types yielding values checked to meet a condition.
|
|
||||||
package check
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"unique"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AbsoluteError is returned by [NewAbs] and holds the invalid pathname.
|
|
||||||
type AbsoluteError string
|
|
||||||
|
|
||||||
func (e AbsoluteError) Error() string {
|
|
||||||
return fmt.Sprintf("path %q is not absolute", string(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e AbsoluteError) Is(target error) bool {
|
|
||||||
var ce AbsoluteError
|
|
||||||
if !errors.As(target, &ce) {
|
|
||||||
return errors.Is(target, syscall.EINVAL)
|
|
||||||
}
|
|
||||||
return e == ce
|
|
||||||
}
|
|
||||||
|
|
||||||
// Absolute holds a pathname checked to be absolute.
|
|
||||||
type Absolute struct{ pathname unique.Handle[string] }
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ encoding.TextAppender = new(Absolute)
|
|
||||||
_ encoding.TextMarshaler = new(Absolute)
|
|
||||||
_ encoding.TextUnmarshaler = new(Absolute)
|
|
||||||
|
|
||||||
_ encoding.BinaryAppender = new(Absolute)
|
|
||||||
_ encoding.BinaryMarshaler = new(Absolute)
|
|
||||||
_ encoding.BinaryUnmarshaler = new(Absolute)
|
|
||||||
)
|
|
||||||
|
|
||||||
// ok returns whether [Absolute] is not the zero value.
|
|
||||||
func (a *Absolute) ok() bool { return a != nil && *a != (Absolute{}) }
|
|
||||||
|
|
||||||
// unsafeAbs returns [check.Absolute] on any string value.
|
|
||||||
func unsafeAbs(pathname string) *Absolute {
|
|
||||||
return &Absolute{unique.Make(pathname)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the checked pathname.
|
|
||||||
func (a *Absolute) String() string {
|
|
||||||
if !a.ok() {
|
|
||||||
panic("attempted use of zero Absolute")
|
|
||||||
}
|
|
||||||
return a.pathname.Value()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle returns the underlying [unique.Handle].
|
|
||||||
func (a *Absolute) Handle() unique.Handle[string] {
|
|
||||||
return a.pathname
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is efficiently compares the underlying pathname.
|
|
||||||
func (a *Absolute) Is(v *Absolute) bool {
|
|
||||||
if a == nil && v == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return a.ok() && v.ok() && a.pathname == v.pathname
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAbs checks pathname and returns a new [Absolute] if pathname is absolute.
|
|
||||||
func NewAbs(pathname string) (*Absolute, error) {
|
|
||||||
if !filepath.IsAbs(pathname) {
|
|
||||||
return nil, AbsoluteError(pathname)
|
|
||||||
}
|
|
||||||
return unsafeAbs(pathname), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustAbs calls [NewAbs] and panics on error.
|
|
||||||
func MustAbs(pathname string) *Absolute {
|
|
||||||
if a, err := NewAbs(pathname); err != nil {
|
|
||||||
panic(err)
|
|
||||||
} else {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append calls [filepath.Join] with [Absolute] as the first element.
|
|
||||||
func (a *Absolute) Append(elem ...string) *Absolute {
|
|
||||||
return unsafeAbs(filepath.Join(append([]string{a.String()}, elem...)...))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dir calls [filepath.Dir] with [Absolute] as its argument.
|
|
||||||
func (a *Absolute) Dir() *Absolute { return unsafeAbs(filepath.Dir(a.String())) }
|
|
||||||
|
|
||||||
// AppendText appends the checked pathname.
|
|
||||||
func (a *Absolute) AppendText(data []byte) ([]byte, error) {
|
|
||||||
return append(data, a.String()...), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalText returns the checked pathname.
|
|
||||||
func (a *Absolute) MarshalText() ([]byte, error) { return a.AppendText(nil) }
|
|
||||||
|
|
||||||
// UnmarshalText stores data if it represents an absolute pathname.
|
|
||||||
func (a *Absolute) UnmarshalText(data []byte) error {
|
|
||||||
pathname := string(data)
|
|
||||||
if !filepath.IsAbs(pathname) {
|
|
||||||
return AbsoluteError(pathname)
|
|
||||||
}
|
|
||||||
a.pathname = unique.Make(pathname)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Absolute) AppendBinary(data []byte) ([]byte, error) { return a.AppendText(data) }
|
|
||||||
func (a *Absolute) MarshalBinary() ([]byte, error) { return a.MarshalText() }
|
|
||||||
func (a *Absolute) UnmarshalBinary(data []byte) error { return a.UnmarshalText(data) }
|
|
||||||
|
|
||||||
// SortAbs calls [slices.SortFunc] for a slice of [Absolute].
|
|
||||||
func SortAbs(x []*Absolute) {
|
|
||||||
slices.SortFunc(x, func(a, b *Absolute) int {
|
|
||||||
return strings.Compare(a.String(), b.String())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// CompactAbs calls [slices.CompactFunc] for a slice of [Absolute].
|
|
||||||
func CompactAbs(s []*Absolute) []*Absolute {
|
|
||||||
return slices.CompactFunc(s, func(a *Absolute, b *Absolute) bool {
|
|
||||||
return a.Is(b)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package check
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
const (
|
|
||||||
// SpecialOverlayEscape is the escape string for overlay mount options.
|
|
||||||
//
|
|
||||||
// Deprecated: This is no longer used and will be removed in 0.5.
|
|
||||||
SpecialOverlayEscape = `\`
|
|
||||||
// SpecialOverlayOption is the separator string between overlay mount options.
|
|
||||||
//
|
|
||||||
// Deprecated: This is no longer used and will be removed in 0.5.
|
|
||||||
SpecialOverlayOption = ","
|
|
||||||
// SpecialOverlayPath is the separator string between overlay paths.
|
|
||||||
//
|
|
||||||
// Deprecated: This is no longer used and will be removed in 0.5.
|
|
||||||
SpecialOverlayPath = ":"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EscapeOverlayDataSegment escapes a string for formatting into the data
|
|
||||||
// argument of an overlay mount system call.
|
|
||||||
//
|
|
||||||
// Deprecated: This is no longer used and will be removed in 0.5.
|
|
||||||
func EscapeOverlayDataSegment(s string) string {
|
|
||||||
if s == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if f := strings.SplitN(s, "\x00", 2); len(f) > 0 {
|
|
||||||
s = f[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.NewReplacer(
|
|
||||||
SpecialOverlayEscape, SpecialOverlayEscape+SpecialOverlayEscape,
|
|
||||||
SpecialOverlayOption, SpecialOverlayEscape+SpecialOverlayOption,
|
|
||||||
SpecialOverlayPath, SpecialOverlayEscape+SpecialOverlayPath,
|
|
||||||
).Replace(s)
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package check_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEscapeOverlayDataSegment(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
s string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{"zero", "", ""},
|
|
||||||
{"multi", `\\\:,:,\\\`, `\\\\\\\:\,\:\,\\\\\\`},
|
|
||||||
{"bwrap", `/path :,\`, `/path \:\,\\`},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
if got := check.EscapeOverlayDataSegment(tc.s); got != tc.want {
|
|
||||||
t.Errorf("escapeOverlayDataSegment: %s, want %s", got, tc.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Vendored
-10
@@ -1,10 +0,0 @@
|
|||||||
#!/bin/sh -e
|
|
||||||
|
|
||||||
TOOLCHAIN_VERSION="$(go version)"
|
|
||||||
cd "$(dirname -- "$0")/../.."
|
|
||||||
echo "Building cmd/dist using ${TOOLCHAIN_VERSION}."
|
|
||||||
FLAGS=''
|
|
||||||
if test -n "$VERBOSE"; then
|
|
||||||
FLAGS="$FLAGS -v"
|
|
||||||
fi
|
|
||||||
go run $FLAGS --tags=dist ./cmd/dist
|
|
||||||
Vendored
-249
@@ -1,249 +0,0 @@
|
|||||||
//go:build dist
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"archive/tar"
|
|
||||||
"compress/gzip"
|
|
||||||
"context"
|
|
||||||
"crypto/sha512"
|
|
||||||
_ "embed"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
// getenv looks up an environment variable, and returns fallback if it is unset.
|
|
||||||
func getenv(key, fallback string) string {
|
|
||||||
if v, ok := os.LookupEnv(key); ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
// mustRun runs a command with the current process's environment and panics
|
|
||||||
// on error or non-zero exit code.
|
|
||||||
func mustRun(ctx context.Context, name string, arg ...string) {
|
|
||||||
cmd := exec.CommandContext(ctx, name, arg...)
|
|
||||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:embed comp/_hakurei
|
|
||||||
var comp []byte
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
log.SetFlags(0)
|
|
||||||
log.SetPrefix("")
|
|
||||||
|
|
||||||
verbose := os.Getenv("VERBOSE") != ""
|
|
||||||
runTests := os.Getenv("HAKUREI_DIST_MAKE") == ""
|
|
||||||
version := getenv("HAKUREI_VERSION", "untagged")
|
|
||||||
prefix := getenv("PREFIX", "/usr")
|
|
||||||
destdir := getenv("DESTDIR", "dist")
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
log.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(destdir, 0755); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
s, err := os.MkdirTemp(destdir, ".dist.*")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
var code int
|
|
||||||
|
|
||||||
if err = os.RemoveAll(s); err != nil {
|
|
||||||
code = 1
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
code = 1
|
|
||||||
log.Println(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Exit(code)
|
|
||||||
}()
|
|
||||||
|
|
||||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
verboseFlag := "-v"
|
|
||||||
if !verbose {
|
|
||||||
verboseFlag = "-buildvcs=false"
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Building hakurei for %s/%s.", runtime.GOOS, runtime.GOARCH)
|
|
||||||
mustRun(ctx, "go", "generate", "./...")
|
|
||||||
mustRun(
|
|
||||||
ctx, "go", "build",
|
|
||||||
"-trimpath",
|
|
||||||
verboseFlag, "-o", s,
|
|
||||||
"-ldflags=-s -w "+
|
|
||||||
"-buildid= -linkmode external -extldflags=-static "+
|
|
||||||
"-X hakurei.app/internal/info.buildVersion="+version+" "+
|
|
||||||
"-X hakurei.app/internal/info.hakureiPath="+prefix+"/bin/hakurei "+
|
|
||||||
"-X hakurei.app/internal/info.hsuPath="+prefix+"/bin/hsu "+
|
|
||||||
"-X main.hakureiPath="+prefix+"/bin/hakurei",
|
|
||||||
"./...",
|
|
||||||
)
|
|
||||||
log.Println()
|
|
||||||
|
|
||||||
if runTests {
|
|
||||||
log.Println("##### Testing Hakurei.")
|
|
||||||
mustRun(
|
|
||||||
ctx, "go", "test",
|
|
||||||
"-ldflags=-buildid= -linkmode external -extldflags=-static",
|
|
||||||
"./...",
|
|
||||||
)
|
|
||||||
log.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("##### Creating distribution.")
|
|
||||||
const suffix = ".tar.gz"
|
|
||||||
distName := "hakurei-" + version + "-" + runtime.GOARCH
|
|
||||||
var f *os.File
|
|
||||||
if f, err = os.OpenFile(
|
|
||||||
filepath.Join(s, distName+suffix),
|
|
||||||
os.O_CREATE|os.O_EXCL|os.O_WRONLY,
|
|
||||||
0644,
|
|
||||||
); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if f == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = f.Close(); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
h := sha512.New()
|
|
||||||
gw, _ := gzip.NewWriterLevel(io.MultiWriter(f, h), gzip.BestCompression)
|
|
||||||
tw := tar.NewWriter(gw)
|
|
||||||
|
|
||||||
mustWriteHeader := func(name string, size int64, mode os.FileMode) {
|
|
||||||
header := tar.Header{
|
|
||||||
Name: filepath.Join(distName, name),
|
|
||||||
Size: size,
|
|
||||||
Mode: int64(mode),
|
|
||||||
Uname: "root",
|
|
||||||
Gname: "root",
|
|
||||||
}
|
|
||||||
|
|
||||||
if mode&os.ModeDir != 0 {
|
|
||||||
header.Typeflag = tar.TypeDir
|
|
||||||
fmt.Printf("%s %s\n", mode, name)
|
|
||||||
} else {
|
|
||||||
header.Typeflag = tar.TypeReg
|
|
||||||
fmt.Printf("%s %s (%d bytes)\n", mode, name, size)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = tw.WriteHeader(&header); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mustWriteFile := func(name string, data []byte, mode os.FileMode) {
|
|
||||||
mustWriteHeader(name, int64(len(data)), mode)
|
|
||||||
if mode&os.ModeDir != 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, err = tw.Write(data); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mustWriteFromPath := func(dst, src string, mode os.FileMode) {
|
|
||||||
var r *os.File
|
|
||||||
if r, err = os.Open(src); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var fi os.FileInfo
|
|
||||||
if fi, err = r.Stat(); err != nil {
|
|
||||||
_ = r.Close()
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mode == 0 {
|
|
||||||
mode = fi.Mode()
|
|
||||||
}
|
|
||||||
|
|
||||||
mustWriteHeader(dst, fi.Size(), mode)
|
|
||||||
if _, err = io.Copy(tw, r); err != nil {
|
|
||||||
_ = r.Close()
|
|
||||||
panic(err)
|
|
||||||
} else if err = r.Close(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mustWriteFile(".", nil, fs.ModeDir|0755)
|
|
||||||
mustWriteFile("comp/", nil, os.ModeDir|0755)
|
|
||||||
mustWriteFile("comp/_hakurei", comp, 0644)
|
|
||||||
mustWriteFile("install.sh", []byte(`#!/bin/sh -e
|
|
||||||
cd "$(dirname -- "$0")" || exit 1
|
|
||||||
|
|
||||||
install -vDm0755 "bin/hakurei" "${DESTDIR}`+prefix+`/bin/hakurei"
|
|
||||||
install -vDm0755 "bin/sharefs" "${DESTDIR}`+prefix+`/bin/sharefs"
|
|
||||||
|
|
||||||
install -vDm4511 "bin/hsu" "${DESTDIR}`+prefix+`/bin/hsu"
|
|
||||||
if [ ! -f "${DESTDIR}/etc/hsurc" ]; then
|
|
||||||
install -vDm0400 "hsurc.default" "${DESTDIR}/etc/hsurc"
|
|
||||||
fi
|
|
||||||
|
|
||||||
install -vDm0644 "comp/_hakurei" "${DESTDIR}`+prefix+`/share/zsh/site-functions/_hakurei"
|
|
||||||
`), 0755)
|
|
||||||
|
|
||||||
mustWriteFromPath("README.md", "README.md", 0)
|
|
||||||
mustWriteFile("hsurc.default", []byte("1000 0"), 0400)
|
|
||||||
mustWriteFromPath("bin/hsu", filepath.Join(s, "hsu"), 04511)
|
|
||||||
for _, name := range []string{
|
|
||||||
"hakurei",
|
|
||||||
"sharefs",
|
|
||||||
} {
|
|
||||||
mustWriteFromPath(
|
|
||||||
filepath.Join("bin", name),
|
|
||||||
filepath.Join(s, name),
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = tw.Close(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
} else if err = gw.Close(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
} else if err = f.Close(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
f = nil
|
|
||||||
|
|
||||||
if err = os.WriteFile(
|
|
||||||
filepath.Join(destdir, distName+suffix+".sha512"),
|
|
||||||
append(hex.AppendEncode(nil, h.Sum(nil)), " "+distName+suffix+"\n"...),
|
|
||||||
0644,
|
|
||||||
); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if err = os.Rename(
|
|
||||||
filepath.Join(s, distName+suffix),
|
|
||||||
filepath.Join(destdir, distName+suffix),
|
|
||||||
); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
// The earlyinit is part of the Rosa OS initramfs and serves as the system init.
|
|
||||||
//
|
|
||||||
// This program is an internal detail of Rosa OS and is not usable on its own.
|
|
||||||
// It is not covered by the compatibility promise.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
. "syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
runtime.LockOSThread()
|
|
||||||
log.SetFlags(0)
|
|
||||||
log.SetPrefix("earlyinit: ")
|
|
||||||
|
|
||||||
var (
|
|
||||||
option map[string]string
|
|
||||||
flags []string
|
|
||||||
)
|
|
||||||
if len(os.Args) > 1 {
|
|
||||||
option = make(map[string]string)
|
|
||||||
for _, s := range os.Args[1:] {
|
|
||||||
key, value, ok := strings.Cut(s, "=")
|
|
||||||
if !ok {
|
|
||||||
flags = append(flags, s)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
option[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := Mount(
|
|
||||||
"devtmpfs",
|
|
||||||
"/dev/",
|
|
||||||
"devtmpfs",
|
|
||||||
MS_NOSUID|MS_NOEXEC,
|
|
||||||
"",
|
|
||||||
); err != nil {
|
|
||||||
log.Fatalf("cannot mount devtmpfs: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The kernel might be unable to set up the console. When that happens,
|
|
||||||
// printk is called with "Warning: unable to open an initial console."
|
|
||||||
// and the init runs with no files. The checkfds runtime function
|
|
||||||
// populates 0-2 by opening /dev/null for them.
|
|
||||||
//
|
|
||||||
// This check replaces 1 and 2 with /dev/kmsg to improve the chance
|
|
||||||
// of output being visible to the user.
|
|
||||||
if fi, err := os.Stdout.Stat(); err == nil {
|
|
||||||
if stat, ok := fi.Sys().(*Stat_t); ok {
|
|
||||||
if stat.Rdev == 0x103 {
|
|
||||||
var fd int
|
|
||||||
if fd, err = Open(
|
|
||||||
"/dev/kmsg",
|
|
||||||
O_WRONLY|O_CLOEXEC,
|
|
||||||
0,
|
|
||||||
); err != nil {
|
|
||||||
log.Fatalf("cannot open kmsg: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = Dup3(fd, Stdout, 0); err != nil {
|
|
||||||
log.Fatalf("cannot open stdout: %v", err)
|
|
||||||
}
|
|
||||||
if err = Dup3(fd, Stderr, 0); err != nil {
|
|
||||||
log.Fatalf("cannot open stderr: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = Close(fd); err != nil {
|
|
||||||
log.Printf("cannot close kmsg: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// staying in rootfs, these are no longer used
|
|
||||||
must(os.Remove("/root"))
|
|
||||||
must(os.Remove("/init"))
|
|
||||||
|
|
||||||
must(os.Mkdir("/proc", 0))
|
|
||||||
mustSyscall("mount proc", Mount(
|
|
||||||
"proc",
|
|
||||||
"/proc",
|
|
||||||
"proc",
|
|
||||||
MS_NOSUID|MS_NOEXEC|MS_NODEV,
|
|
||||||
"hidepid=1",
|
|
||||||
))
|
|
||||||
|
|
||||||
must(os.Mkdir("/sys", 0))
|
|
||||||
mustSyscall("mount sysfs", Mount(
|
|
||||||
"sysfs",
|
|
||||||
"/sys",
|
|
||||||
"sysfs",
|
|
||||||
0,
|
|
||||||
"",
|
|
||||||
))
|
|
||||||
|
|
||||||
// after top level has been set up
|
|
||||||
mustSyscall("remount root", Mount(
|
|
||||||
"",
|
|
||||||
"/",
|
|
||||||
"",
|
|
||||||
MS_REMOUNT|MS_BIND|
|
|
||||||
MS_RDONLY|MS_NODEV|MS_NOSUID|MS_NOEXEC,
|
|
||||||
"",
|
|
||||||
))
|
|
||||||
|
|
||||||
must(os.WriteFile(
|
|
||||||
"/sys/module/firmware_class/parameters/path",
|
|
||||||
[]byte("/system/lib/firmware"),
|
|
||||||
0,
|
|
||||||
))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// mustSyscall calls [log.Fatalln] if err is non-nil.
|
|
||||||
func mustSyscall(action string, err error) {
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("cannot "+action+":", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// must calls [log.Fatal] with err if it is non-nil.
|
|
||||||
func must(err error) {
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+171
-296
@@ -2,117 +2,77 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/signal"
|
||||||
"os/user"
|
"os/user"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
"hakurei.app/command"
|
"hakurei.app/command"
|
||||||
"hakurei.app/ext"
|
"hakurei.app/container"
|
||||||
"hakurei.app/fhs"
|
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
"hakurei.app/internal/dbus"
|
"hakurei.app/internal"
|
||||||
"hakurei.app/internal/env"
|
"hakurei.app/internal/app"
|
||||||
"hakurei.app/internal/info"
|
"hakurei.app/internal/app/state"
|
||||||
"hakurei.app/internal/outcome"
|
"hakurei.app/internal/hlog"
|
||||||
"hakurei.app/message"
|
"hakurei.app/system"
|
||||||
|
"hakurei.app/system/dbus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// optionalErrorUnwrap calls [errors.Unwrap] and returns the resulting value
|
func buildCommand(out io.Writer) command.Command {
|
||||||
// if it is not nil, or the original value if it is.
|
|
||||||
func optionalErrorUnwrap(err error) error {
|
|
||||||
if underlyingErr := errors.Unwrap(err); underlyingErr != nil {
|
|
||||||
return underlyingErr
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var errSuccess = errors.New("success")
|
|
||||||
|
|
||||||
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
|
|
||||||
var (
|
var (
|
||||||
flagVerbose bool
|
flagVerbose bool
|
||||||
flagInsecure bool
|
flagJSON bool
|
||||||
flagJSON bool
|
|
||||||
)
|
)
|
||||||
c := command.New(out, log.Printf, "hakurei", func([]string) error {
|
c := command.New(out, log.Printf, "hakurei", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
|
||||||
msg.SwapVerbose(flagVerbose)
|
|
||||||
|
|
||||||
if early.yamaLSM != nil {
|
|
||||||
msg.Verbosef("cannot enable ptrace protection via Yama LSM: %v", early.yamaLSM)
|
|
||||||
// not fatal
|
|
||||||
}
|
|
||||||
|
|
||||||
if early.dumpable != nil {
|
|
||||||
log.Printf("cannot set SUID_DUMP_DISABLE: %s", early.dumpable)
|
|
||||||
// not fatal
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}).
|
|
||||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
|
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
|
||||||
Flag(&flagInsecure, "insecure", command.BoolFlag(false), "Allow use of insecure compatibility options").
|
|
||||||
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
|
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
|
||||||
|
|
||||||
c.Command("shim", command.UsageInternal, func([]string) error { outcome.Shim(msg); return errSuccess })
|
c.Command("shim", command.UsageInternal, func([]string) error { app.ShimMain(); return errSuccess })
|
||||||
|
|
||||||
|
c.Command("app", "Load app from configuration file", func(args []string) error {
|
||||||
|
if len(args) < 1 {
|
||||||
|
log.Fatal("app requires at least 1 argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
// config extraArgs...
|
||||||
|
config := tryPath(args[0])
|
||||||
|
config.Args = append(config.Args, args[1:]...)
|
||||||
|
|
||||||
|
runApp(config)
|
||||||
|
panic("unreachable")
|
||||||
|
})
|
||||||
|
|
||||||
{
|
{
|
||||||
var (
|
var (
|
||||||
flagIdentifierFile int
|
dbusConfigSession string
|
||||||
)
|
dbusConfigSystem string
|
||||||
c.NewCommand("run", "Load and start container from configuration file", func(args []string) error {
|
mpris bool
|
||||||
if len(args) < 1 {
|
dbusVerbose bool
|
||||||
log.Fatal("run requires at least 1 argument")
|
|
||||||
}
|
|
||||||
|
|
||||||
config := tryPath(msg, args[0])
|
fid string
|
||||||
if config != nil && config.Container != nil {
|
aid int
|
||||||
config.Container.Args = append(config.Container.Args, args[1:]...)
|
groups command.RepeatableFlag
|
||||||
}
|
homeDir string
|
||||||
|
userName string
|
||||||
|
|
||||||
var flags int
|
wayland, x11, dBus, pulse bool
|
||||||
if flagInsecure {
|
|
||||||
flags |= hst.VAllowInsecure
|
|
||||||
}
|
|
||||||
|
|
||||||
outcome.Main(ctx, msg, config, flags, flagIdentifierFile)
|
|
||||||
panic("unreachable")
|
|
||||||
}).
|
|
||||||
Flag(&flagIdentifierFile, "identifier-fd", command.IntFlag(-1),
|
|
||||||
"Write identifier of current instance to fd after successful startup")
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
var (
|
|
||||||
flagDBusConfigSession string
|
|
||||||
flagDBusConfigSystem string
|
|
||||||
flagDBusMpris bool
|
|
||||||
flagDBusVerbose bool
|
|
||||||
|
|
||||||
flagID string
|
|
||||||
flagIdentity int
|
|
||||||
flagGroups command.RepeatableFlag
|
|
||||||
flagHomeDir string
|
|
||||||
flagUserName string
|
|
||||||
|
|
||||||
flagSchedPolicy string
|
|
||||||
flagSchedPriority int
|
|
||||||
|
|
||||||
flagPrivateRuntime, flagPrivateTmpdir bool
|
|
||||||
|
|
||||||
flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
c.NewCommand("exec", "Configure and start a permissive container", func(args []string) error {
|
c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
|
||||||
if flagIdentity < hst.IdentityStart || flagIdentity > hst.IdentityEnd {
|
// initialise config from flags
|
||||||
log.Fatalf("identity %d out of range", flagIdentity)
|
config := &hst.Config{
|
||||||
|
ID: fid,
|
||||||
|
Args: args,
|
||||||
|
}
|
||||||
|
|
||||||
|
if aid < 0 || aid > 9999 {
|
||||||
|
log.Fatalf("aid %d out of range", aid)
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolve home/username from os when flag is unset
|
// resolve home/username from os when flag is unset
|
||||||
@@ -120,15 +80,22 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
|||||||
passwd *user.User
|
passwd *user.User
|
||||||
passwdOnce sync.Once
|
passwdOnce sync.Once
|
||||||
passwdFunc = func() {
|
passwdFunc = func() {
|
||||||
us := strconv.Itoa(hst.ToUser(new(outcome.Hsu).MustID(msg), flagIdentity))
|
var us string
|
||||||
|
if uid, err := std.Uid(aid); err != nil {
|
||||||
|
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
|
||||||
|
os.Exit(1)
|
||||||
|
} else {
|
||||||
|
us = strconv.Itoa(uid)
|
||||||
|
}
|
||||||
|
|
||||||
if u, err := user.LookupId(us); err != nil {
|
if u, err := user.LookupId(us); err != nil {
|
||||||
msg.Verbosef("cannot look up uid %s", us)
|
hlog.Verbosef("cannot look up uid %s", us)
|
||||||
passwd = &user.User{
|
passwd = &user.User{
|
||||||
Uid: us,
|
Uid: us,
|
||||||
Gid: us,
|
Gid: us,
|
||||||
Username: "chronos",
|
Username: "chronos",
|
||||||
Name: "Hakurei Permissive Default",
|
Name: "Hakurei Permissive Default",
|
||||||
HomeDir: fhs.VarEmpty,
|
HomeDir: container.FHSVarEmpty,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
passwd = u
|
passwd = u
|
||||||
@@ -136,256 +103,164 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// paths are identical, resolve inner shell and program path
|
if homeDir == "os" {
|
||||||
shell := fhs.AbsRoot.Append("bin", "sh")
|
|
||||||
if a, err := check.NewAbs(os.Getenv("SHELL")); err == nil {
|
|
||||||
shell = a
|
|
||||||
}
|
|
||||||
progPath := shell
|
|
||||||
if len(args) > 0 {
|
|
||||||
if p, err := exec.LookPath(args[0]); err != nil {
|
|
||||||
log.Fatal(optionalErrorUnwrap(err))
|
|
||||||
return err
|
|
||||||
} else if progPath, err = check.NewAbs(p); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var et hst.Enablements
|
|
||||||
if flagWayland {
|
|
||||||
et |= hst.EWayland
|
|
||||||
}
|
|
||||||
if flagX11 {
|
|
||||||
et |= hst.EX11
|
|
||||||
}
|
|
||||||
if flagDBus {
|
|
||||||
et |= hst.EDBus
|
|
||||||
}
|
|
||||||
if flagPipeWire || flagPulse {
|
|
||||||
et |= hst.EPipeWire
|
|
||||||
}
|
|
||||||
|
|
||||||
config := hst.Config{
|
|
||||||
ID: flagID,
|
|
||||||
Identity: flagIdentity,
|
|
||||||
Groups: flagGroups,
|
|
||||||
Enablements: &et,
|
|
||||||
|
|
||||||
Container: &hst.ContainerConfig{
|
|
||||||
Filesystem: []hst.FilesystemConfigJSON{
|
|
||||||
// autoroot, includes the home directory
|
|
||||||
{FilesystemConfig: &hst.FSBind{
|
|
||||||
Target: fhs.AbsRoot,
|
|
||||||
Source: fhs.AbsRoot,
|
|
||||||
Write: true,
|
|
||||||
Special: true,
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
|
|
||||||
Username: flagUserName,
|
|
||||||
Shell: shell,
|
|
||||||
|
|
||||||
Path: progPath,
|
|
||||||
Args: args,
|
|
||||||
|
|
||||||
Flags: hst.FUserns | hst.FHostNet | hst.FHostAbstract | hst.FTty,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := config.SchedPolicy.UnmarshalText(
|
|
||||||
[]byte(flagSchedPolicy),
|
|
||||||
); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
config.SchedPriority = ext.Int(flagSchedPriority)
|
|
||||||
|
|
||||||
// bind GPU stuff
|
|
||||||
if et&(hst.EX11|hst.EWayland) != 0 {
|
|
||||||
config.Container.Filesystem = append(config.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
|
|
||||||
Source: fhs.AbsDev.Append("dri"),
|
|
||||||
Device: true,
|
|
||||||
Optional: true,
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Container.Filesystem = append(config.Container.Filesystem,
|
|
||||||
// opportunistically bind kvm
|
|
||||||
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
|
|
||||||
Source: fhs.AbsDev.Append("kvm"),
|
|
||||||
Device: true,
|
|
||||||
Optional: true,
|
|
||||||
}},
|
|
||||||
|
|
||||||
// do autoetc last
|
|
||||||
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
|
|
||||||
Target: fhs.AbsEtc,
|
|
||||||
Source: fhs.AbsEtc,
|
|
||||||
Special: true,
|
|
||||||
}},
|
|
||||||
)
|
|
||||||
|
|
||||||
if config.Container.Username == "chronos" {
|
|
||||||
passwdOnce.Do(passwdFunc)
|
passwdOnce.Do(passwdFunc)
|
||||||
config.Container.Username = passwd.Username
|
homeDir = passwd.HomeDir
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
if userName == "chronos" {
|
||||||
homeDir := flagHomeDir
|
passwdOnce.Do(passwdFunc)
|
||||||
if homeDir == "os" {
|
userName = passwd.Username
|
||||||
passwdOnce.Do(passwdFunc)
|
|
||||||
homeDir = passwd.HomeDir
|
|
||||||
}
|
|
||||||
if a, err := check.NewAbs(homeDir); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
config.Container.Home = a
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !flagPrivateRuntime {
|
config.Identity = aid
|
||||||
config.Container.Flags |= hst.FShareRuntime
|
config.Groups = groups
|
||||||
|
config.Username = userName
|
||||||
|
|
||||||
|
if a, err := container.NewAbs(homeDir); err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
config.Home = a
|
||||||
}
|
}
|
||||||
if !flagPrivateTmpdir {
|
|
||||||
config.Container.Flags |= hst.FShareTmpdir
|
var e system.Enablement
|
||||||
|
if wayland {
|
||||||
|
e |= system.EWayland
|
||||||
}
|
}
|
||||||
|
if x11 {
|
||||||
|
e |= system.EX11
|
||||||
|
}
|
||||||
|
if dBus {
|
||||||
|
e |= system.EDBus
|
||||||
|
}
|
||||||
|
if pulse {
|
||||||
|
e |= system.EPulse
|
||||||
|
}
|
||||||
|
config.Enablements = hst.NewEnablements(e)
|
||||||
|
|
||||||
// parse D-Bus config file from flags if applicable
|
// parse D-Bus config file from flags if applicable
|
||||||
if flagDBus {
|
if dBus {
|
||||||
if flagDBusConfigSession == "builtin" {
|
if dbusConfigSession == "builtin" {
|
||||||
config.SessionBus = dbus.NewConfig(flagID, true, flagDBusMpris)
|
config.SessionBus = dbus.NewConfig(fid, true, mpris)
|
||||||
} else {
|
} else {
|
||||||
if f, err := os.Open(flagDBusConfigSession); err != nil {
|
if conf, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
|
||||||
} else {
|
} else {
|
||||||
decodeJSON(log.Fatal, "load session bus proxy config", f, &config.SessionBus)
|
config.SessionBus = conf
|
||||||
if err = f.Close(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// system bus proxy is optional
|
// system bus proxy is optional
|
||||||
if flagDBusConfigSystem != "nil" {
|
if dbusConfigSystem != "nil" {
|
||||||
if f, err := os.Open(flagDBusConfigSystem); err != nil {
|
if conf, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
|
||||||
} else {
|
} else {
|
||||||
decodeJSON(log.Fatal, "load system bus proxy config", f, &config.SystemBus)
|
config.SystemBus = conf
|
||||||
if err = f.Close(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// override log from configuration
|
// override log from configuration
|
||||||
if flagDBusVerbose {
|
if dbusVerbose {
|
||||||
if config.SessionBus != nil {
|
config.SessionBus.Log = true
|
||||||
config.SessionBus.Log = true
|
config.SystemBus.Log = true
|
||||||
}
|
|
||||||
if config.SystemBus != nil {
|
|
||||||
config.SystemBus.Log = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
outcome.Main(ctx, msg, &config, 0, -1)
|
// invoke app
|
||||||
|
runApp(config)
|
||||||
panic("unreachable")
|
panic("unreachable")
|
||||||
}).
|
}).
|
||||||
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
|
Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
|
||||||
"Path to session bus proxy config file, or \"builtin\" for defaults").
|
"Path to session bus proxy config file, or \"builtin\" for defaults").
|
||||||
Flag(&flagDBusConfigSystem, "dbus-system", command.StringFlag("nil"),
|
Flag(&dbusConfigSystem, "dbus-system", command.StringFlag("nil"),
|
||||||
"Path to system bus proxy config file, or \"nil\" to disable").
|
"Path to system bus proxy config file, or \"nil\" to disable").
|
||||||
Flag(&flagDBusMpris, "mpris", command.BoolFlag(false),
|
Flag(&mpris, "mpris", command.BoolFlag(false),
|
||||||
"Allow owning MPRIS D-Bus path, has no effect if custom config is available").
|
"Allow owning MPRIS D-Bus path, has no effect if custom config is available").
|
||||||
Flag(&flagDBusVerbose, "dbus-log", command.BoolFlag(false),
|
Flag(&dbusVerbose, "dbus-log", command.BoolFlag(false),
|
||||||
"Force buffered logging in the D-Bus proxy").
|
"Force buffered logging in the D-Bus proxy").
|
||||||
Flag(&flagID, "id", command.StringFlag(""),
|
Flag(&fid, "id", command.StringFlag(""),
|
||||||
"Reverse-DNS style Application identifier, leave empty to inherit instance identifier").
|
"Reverse-DNS style Application identifier, leave empty to inherit instance identifier").
|
||||||
Flag(&flagIdentity, "a", command.IntFlag(0),
|
Flag(&aid, "a", command.IntFlag(0),
|
||||||
"Application identity").
|
"Application identity").
|
||||||
Flag(nil, "g", &flagGroups,
|
Flag(nil, "g", &groups,
|
||||||
"Groups inherited by all container processes").
|
"Groups inherited by all container processes").
|
||||||
Flag(&flagHomeDir, "d", command.StringFlag("os"),
|
Flag(&homeDir, "d", command.StringFlag("os"),
|
||||||
"Container home directory").
|
"Container home directory").
|
||||||
Flag(&flagUserName, "u", command.StringFlag("chronos"),
|
Flag(&userName, "u", command.StringFlag("chronos"),
|
||||||
"Passwd user name within sandbox").
|
"Passwd user name within sandbox").
|
||||||
Flag(&flagSchedPolicy, "policy", command.StringFlag(""),
|
Flag(&wayland, "wayland", command.BoolFlag(false),
|
||||||
"Scheduling policy to set for the container").
|
|
||||||
Flag(&flagSchedPriority, "priority", command.IntFlag(0),
|
|
||||||
"Scheduling priority to set for the container").
|
|
||||||
Flag(&flagPrivateRuntime, "private-runtime", command.BoolFlag(false),
|
|
||||||
"Do not share XDG_RUNTIME_DIR between containers under the same identity").
|
|
||||||
Flag(&flagPrivateTmpdir, "private-tmpdir", command.BoolFlag(false),
|
|
||||||
"Do not share TMPDIR between containers under the same identity").
|
|
||||||
Flag(&flagWayland, "wayland", command.BoolFlag(false),
|
|
||||||
"Enable connection to Wayland via security-context-v1").
|
"Enable connection to Wayland via security-context-v1").
|
||||||
Flag(&flagX11, "X", command.BoolFlag(false),
|
Flag(&x11, "X", command.BoolFlag(false),
|
||||||
"Enable direct connection to X11").
|
"Enable direct connection to X11").
|
||||||
Flag(&flagDBus, "dbus", command.BoolFlag(false),
|
Flag(&dBus, "dbus", command.BoolFlag(false),
|
||||||
"Enable proxied connection to D-Bus").
|
"Enable proxied connection to D-Bus").
|
||||||
Flag(&flagPipeWire, "pipewire", command.BoolFlag(false),
|
Flag(&pulse, "pulse", command.BoolFlag(false),
|
||||||
"Enable connection to PipeWire via SecurityContext").
|
"Enable direct connection to PulseAudio")
|
||||||
Flag(&flagPulse, "pulse", command.BoolFlag(false),
|
|
||||||
"Enable PulseAudio compatibility daemon")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
var showFlagShort bool
|
||||||
var (
|
c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
|
||||||
flagShort bool
|
switch len(args) {
|
||||||
flagNoStore bool
|
case 0: // system
|
||||||
)
|
printShowSystem(os.Stdout, showFlagShort, flagJSON)
|
||||||
c.NewCommand("show", "Show live or local instance configuration", func(args []string) error {
|
|
||||||
switch len(args) {
|
|
||||||
case 0: // system
|
|
||||||
printShowSystem(os.Stdout, flagShort, flagJSON)
|
|
||||||
|
|
||||||
case 1: // instance
|
case 1: // instance
|
||||||
name := args[0]
|
name := args[0]
|
||||||
|
config, entry := tryShort(name)
|
||||||
var (
|
if config == nil {
|
||||||
config *hst.Config
|
config = tryPath(name)
|
||||||
entry *hst.State
|
|
||||||
)
|
|
||||||
if !flagNoStore {
|
|
||||||
var sc hst.Paths
|
|
||||||
env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
|
|
||||||
entry = tryIdentifier(msg, name, outcome.NewStore(&sc))
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry == nil {
|
|
||||||
config = tryPath(msg, name)
|
|
||||||
} else {
|
|
||||||
config = entry.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
if !printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON) {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
log.Fatal("show requires 1 argument")
|
|
||||||
}
|
}
|
||||||
return errSuccess
|
printShowInstance(os.Stdout, time.Now().UTC(), entry, config, showFlagShort, flagJSON)
|
||||||
}).
|
|
||||||
Flag(&flagShort, "short", command.BoolFlag(false), "Omit filesystem information").
|
|
||||||
Flag(&flagNoStore, "no-store", command.BoolFlag(false), "Do not attempt to match from active instances")
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
default:
|
||||||
var flagShort bool
|
log.Fatal("show requires 1 argument")
|
||||||
c.NewCommand("ps", "List active instances", func(args []string) error {
|
}
|
||||||
var sc hst.Paths
|
return errSuccess
|
||||||
env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
|
}).Flag(&showFlagShort, "short", command.BoolFlag(false), "Omit filesystem information")
|
||||||
printPs(msg, os.Stdout, time.Now().UTC(), outcome.NewStore(&sc), flagShort, flagJSON)
|
|
||||||
return errSuccess
|
|
||||||
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Command("version", "Display version information", func(args []string) error { fmt.Println(info.Version()); return errSuccess })
|
var psFlagShort bool
|
||||||
c.Command("license", "Show full license text", func(args []string) error { fmt.Println(license); return errSuccess })
|
c.NewCommand("ps", "List active instances", func(args []string) error {
|
||||||
c.Command("template", "Produce a config template", func(args []string) error { encodeJSON(log.Fatal, os.Stdout, false, hst.Template()); return errSuccess })
|
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath.String()), psFlagShort, flagJSON)
|
||||||
c.Command("help", "Show this help message", func([]string) error { c.PrintHelp(); return errSuccess })
|
return errSuccess
|
||||||
|
}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")
|
||||||
|
|
||||||
|
c.Command("version", "Display version information", func(args []string) error {
|
||||||
|
fmt.Println(internal.Version())
|
||||||
|
return errSuccess
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Command("license", "Show full license text", func(args []string) error {
|
||||||
|
fmt.Println(license)
|
||||||
|
return errSuccess
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Command("template", "Produce a config template", func(args []string) error {
|
||||||
|
printJSON(os.Stdout, false, hst.Template())
|
||||||
|
return errSuccess
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Command("help", "Show this help message", func([]string) error {
|
||||||
|
c.PrintHelp()
|
||||||
|
return errSuccess
|
||||||
|
})
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runApp(config *hst.Config) {
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(),
|
||||||
|
syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer stop() // unreachable
|
||||||
|
a := app.MustNew(ctx, std)
|
||||||
|
|
||||||
|
rs := new(app.RunState)
|
||||||
|
if sa, err := a.Seal(config); err != nil {
|
||||||
|
hlog.PrintBaseError(err, "cannot seal app:")
|
||||||
|
internal.Exit(1)
|
||||||
|
} else {
|
||||||
|
internal.Exit(app.PrintRunStateErr(rs, sa.Run(rs)))
|
||||||
|
}
|
||||||
|
|
||||||
|
*(*int)(nil) = 0 // not reached
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,12 +7,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"hakurei.app/command"
|
"hakurei.app/command"
|
||||||
"hakurei.app/message"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHelp(t *testing.T) {
|
func TestHelp(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
args []string
|
args []string
|
||||||
@@ -20,12 +17,12 @@ func TestHelp(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
"main", []string{}, `
|
"main", []string{}, `
|
||||||
Usage: hakurei [-h | --help] [-v] [--insecure] [--json] COMMAND [OPTIONS]
|
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
run Load and start container from configuration file
|
app Load app from configuration file
|
||||||
exec Configure and start a permissive container
|
run Configure and start a permissive default sandbox
|
||||||
show Show live or local instance configuration
|
show Show live or local app configuration
|
||||||
ps List active instances
|
ps List active instances
|
||||||
version Display version information
|
version Display version information
|
||||||
license Show full license text
|
license Show full license text
|
||||||
@@ -35,8 +32,8 @@ Commands:
|
|||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"exec", []string{"exec", "-h"}, `
|
"run", []string{"run", "-h"}, `
|
||||||
Usage: hakurei exec [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--policy <value>] [--priority <int>] [--private-runtime] [--private-tmpdir] [--wayland] [-X] [--dbus] [--pipewire] [--pulse] COMMAND [OPTIONS]
|
Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--wayland] [-X] [--dbus] [--pulse] COMMAND [OPTIONS]
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
-X Enable direct connection to X11
|
-X Enable direct connection to X11
|
||||||
@@ -58,18 +55,8 @@ Flags:
|
|||||||
Reverse-DNS style Application identifier, leave empty to inherit instance identifier
|
Reverse-DNS style Application identifier, leave empty to inherit instance identifier
|
||||||
-mpris
|
-mpris
|
||||||
Allow owning MPRIS D-Bus path, has no effect if custom config is available
|
Allow owning MPRIS D-Bus path, has no effect if custom config is available
|
||||||
-pipewire
|
|
||||||
Enable connection to PipeWire via SecurityContext
|
|
||||||
-policy string
|
|
||||||
Scheduling policy to set for the container
|
|
||||||
-priority int
|
|
||||||
Scheduling priority to set for the container
|
|
||||||
-private-runtime
|
|
||||||
Do not share XDG_RUNTIME_DIR between containers under the same identity
|
|
||||||
-private-tmpdir
|
|
||||||
Do not share TMPDIR between containers under the same identity
|
|
||||||
-pulse
|
-pulse
|
||||||
Enable PulseAudio compatibility daemon
|
Enable direct connection to PulseAudio
|
||||||
-u string
|
-u string
|
||||||
Passwd user name within sandbox (default "chronos")
|
Passwd user name within sandbox (default "chronos")
|
||||||
-wayland
|
-wayland
|
||||||
@@ -80,10 +67,8 @@ Flags:
|
|||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
out := new(bytes.Buffer)
|
out := new(bytes.Buffer)
|
||||||
c := buildCommand(t.Context(), message.New(nil), new(earlyHardeningErrs), out)
|
c := buildCommand(out)
|
||||||
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
|
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
|
||||||
t.Errorf("Parse: error = %v; want %v",
|
t.Errorf("Parse: error = %v; want %v",
|
||||||
err, command.ErrHelp)
|
err, command.ErrHelp)
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// decodeJSON decodes json from r and stores it in v. A non-nil error results in a call to fatal.
|
|
||||||
func decodeJSON(fatal func(v ...any), op string, r io.Reader, v any) {
|
|
||||||
err := json.NewDecoder(r).Decode(v)
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
syntaxError *json.SyntaxError
|
|
||||||
unmarshalTypeError *json.UnmarshalTypeError
|
|
||||||
|
|
||||||
msg string
|
|
||||||
)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case errors.As(err, &syntaxError) && syntaxError != nil:
|
|
||||||
msg = syntaxError.Error() +
|
|
||||||
" at byte " + strconv.FormatInt(syntaxError.Offset, 10)
|
|
||||||
|
|
||||||
case errors.As(err, &unmarshalTypeError) && unmarshalTypeError != nil:
|
|
||||||
msg = "inappropriate " + unmarshalTypeError.Value +
|
|
||||||
" at byte " + strconv.FormatInt(unmarshalTypeError.Offset, 10)
|
|
||||||
|
|
||||||
default:
|
|
||||||
// InvalidUnmarshalError: incorrect usage, does not need to be handled
|
|
||||||
// io.ErrUnexpectedEOF: no additional error information available
|
|
||||||
msg = err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
fatal("cannot " + op + ": " + msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// encodeJSON encodes v to output. A non-nil error results in a call to fatal.
|
|
||||||
func encodeJSON(fatal func(v ...any), output io.Writer, short bool, v any) {
|
|
||||||
encoder := json.NewEncoder(output)
|
|
||||||
if !short {
|
|
||||||
encoder.SetIndent("", " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := encoder.Encode(v); err != nil {
|
|
||||||
var marshalerError *json.MarshalerError
|
|
||||||
if errors.As(err, &marshalerError) && marshalerError != nil {
|
|
||||||
// this likely indicates an implementation error in hst
|
|
||||||
fatal("cannot encode json for " + marshalerError.Type.String() + ": " + marshalerError.Err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnsupportedTypeError, UnsupportedValueError: incorrect usage, does not need to be handled
|
|
||||||
fatal("cannot write json: " + err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"hakurei.app/internal/stub"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDecodeJSON(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
t reflect.Type
|
|
||||||
data string
|
|
||||||
want any
|
|
||||||
msg string
|
|
||||||
}{
|
|
||||||
{"success", reflect.TypeFor[uintptr](), "3735928559\n", uintptr(0xdeadbeef), ""},
|
|
||||||
|
|
||||||
{"syntax", reflect.TypeFor[*int](), "\x00", nil,
|
|
||||||
`cannot load sample: invalid character '\x00' looking for beginning of value at byte 1`},
|
|
||||||
{"type", reflect.TypeFor[uintptr](), "-1", nil,
|
|
||||||
`cannot load sample: inappropriate number -1 at byte 2`},
|
|
||||||
{"default", reflect.TypeFor[*int](), "{", nil,
|
|
||||||
"cannot load sample: unexpected EOF"},
|
|
||||||
}
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
gotP = reflect.New(tc.t)
|
|
||||||
gotMsg *string
|
|
||||||
)
|
|
||||||
decodeJSON(func(v ...any) {
|
|
||||||
if gotMsg != nil {
|
|
||||||
t.Fatal("fatal called twice")
|
|
||||||
}
|
|
||||||
msg := v[0].(string)
|
|
||||||
gotMsg = &msg
|
|
||||||
}, "load sample", strings.NewReader(tc.data), gotP.Interface())
|
|
||||||
if tc.msg != "" {
|
|
||||||
if gotMsg == nil {
|
|
||||||
t.Errorf("decodeJSON: success, want fatal %q", tc.msg)
|
|
||||||
} else if *gotMsg != tc.msg {
|
|
||||||
t.Errorf("decodeJSON: fatal = %q, want %q", *gotMsg, tc.msg)
|
|
||||||
}
|
|
||||||
} else if gotMsg != nil {
|
|
||||||
t.Errorf("decodeJSON: fatal = %q", *gotMsg)
|
|
||||||
} else if !reflect.DeepEqual(gotP.Elem().Interface(), tc.want) {
|
|
||||||
t.Errorf("decodeJSON: %#v, want %#v", gotP.Elem().Interface(), tc.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEncodeJSON(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
v any
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{"marshaler", errorJSONMarshaler{},
|
|
||||||
`cannot encode json for main.errorJSONMarshaler: unique error 3735928559 injected by the test suite`},
|
|
||||||
{"default", func() {},
|
|
||||||
`cannot write json: json: unsupported type: func()`},
|
|
||||||
}
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var called bool
|
|
||||||
encodeJSON(func(v ...any) {
|
|
||||||
if called {
|
|
||||||
t.Fatal("fatal called twice")
|
|
||||||
}
|
|
||||||
called = true
|
|
||||||
|
|
||||||
if v[0].(string) != tc.want {
|
|
||||||
t.Errorf("encodeJSON: fatal = %q, want %q", v[0].(string), tc.want)
|
|
||||||
}
|
|
||||||
}, nil, false, tc.v)
|
|
||||||
|
|
||||||
if !called {
|
|
||||||
t.Errorf("encodeJSON: success, want fatal %q", tc.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// errorJSONMarshaler implements json.Marshaler.
|
|
||||||
type errorJSONMarshaler struct{}
|
|
||||||
|
|
||||||
func (errorJSONMarshaler) MarshalJSON() ([]byte, error) { return nil, stub.UniqueError(0xdeadbeef) }
|
|
||||||
+26
-61
@@ -1,88 +1,53 @@
|
|||||||
// Hakurei runs user-specified containers as subordinate users.
|
|
||||||
//
|
|
||||||
// This program is generally invoked by another, higher level program, which
|
|
||||||
// creates container configuration via package [hst] or an implementation of it.
|
|
||||||
//
|
|
||||||
// The parent may leave files open and specify their file descriptor for various
|
|
||||||
// uses. In these cases, standard streams and netpoll files are treated as
|
|
||||||
// invalid file descriptors and rejected. All string representations must be in
|
|
||||||
// decimal.
|
|
||||||
//
|
|
||||||
// When specifying a [hst.Config] JSON stream or file to the run subcommand, the
|
|
||||||
// argument "-" is equivalent to stdin. Otherwise, file descriptor rules
|
|
||||||
// described above applies. Invalid file descriptors are treated as file names
|
|
||||||
// in their string representation, with the exception that if a netpoll file
|
|
||||||
// descriptor is attempted, the program fails.
|
|
||||||
//
|
|
||||||
// The flag --identifier-fd can be optionally specified to the run subcommand to
|
|
||||||
// receive the identifier of the newly started instance. File descriptor rules
|
|
||||||
// described above applies, and the file must be writable. This is sent after
|
|
||||||
// its state is made available, so the client must not attempt to poll for it.
|
|
||||||
// This uses the internal binary format of [hst.ID].
|
|
||||||
//
|
|
||||||
// For the show and ps subcommands, the flag --json can be applied to the main
|
|
||||||
// hakurei command to serialise output in JSON when applicable. Additionally,
|
|
||||||
// the flag --short targeting each subcommand is used to omit some information
|
|
||||||
// in both JSON and user-facing output. Only JSON-encoded output is covered
|
|
||||||
// under the compatibility promise.
|
|
||||||
//
|
|
||||||
// A template for [hst.Config] demonstrating all available configuration fields
|
|
||||||
// is returned by [hst.Template]. The JSON-encoded equivalent of this can be
|
|
||||||
// obtained via the template subcommand. Fields left unpopulated in the template
|
|
||||||
// (the direct_* family of fields, which are insecure under any configuration if
|
|
||||||
// enabled) are unsupported.
|
|
||||||
//
|
|
||||||
// For simple (but insecure) testing scenarios, the exec subcommand can be used
|
|
||||||
// to generate a simple, permissive configuration in-memory. See its help
|
|
||||||
// message for all available options.
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
// this works around go:embed '..' limitation
|
||||||
|
//go:generate cp ../../LICENSE .
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"hakurei.app/container"
|
"hakurei.app/container"
|
||||||
"hakurei.app/ext"
|
"hakurei.app/internal"
|
||||||
"hakurei.app/message"
|
"hakurei.app/internal/hlog"
|
||||||
|
"hakurei.app/internal/sys"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate cp ../../LICENSE .
|
var (
|
||||||
//go:embed LICENSE
|
errSuccess = errors.New("success")
|
||||||
var license string
|
|
||||||
|
|
||||||
// earlyHardeningErrs are errors collected while setting up early hardening feature.
|
//go:embed LICENSE
|
||||||
type earlyHardeningErrs struct{ yamaLSM, dumpable error }
|
license string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() { hlog.Prepare("hakurei") }
|
||||||
|
|
||||||
|
var std sys.State = new(sys.Std)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// early init path, skips root check and duplicate PR_SET_DUMPABLE
|
// early init path, skips root check and duplicate PR_SET_DUMPABLE
|
||||||
container.TryArgv0(nil)
|
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
|
||||||
|
|
||||||
log.SetFlags(0)
|
if err := container.SetPtracer(0); err != nil {
|
||||||
log.SetPrefix("hakurei: ")
|
hlog.Verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
|
||||||
msg := message.New(log.Default())
|
// not fatal: this program runs as the privileged user
|
||||||
|
}
|
||||||
|
|
||||||
early := earlyHardeningErrs{
|
if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil {
|
||||||
yamaLSM: ext.SetPtracer(0),
|
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||||
dumpable: ext.SetDumpable(ext.SUID_DUMP_DISABLE),
|
// not fatal: this program runs as the privileged user
|
||||||
}
|
}
|
||||||
|
|
||||||
if os.Geteuid() == 0 {
|
if os.Geteuid() == 0 {
|
||||||
log.Fatal("this program must not run as root")
|
log.Fatal("this program must not run as root")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(),
|
buildCommand(os.Stderr).MustParse(os.Args[1:], func(err error) {
|
||||||
syscall.SIGINT, syscall.SIGTERM)
|
hlog.Verbosef("command returned %v", err)
|
||||||
defer stop() // unreachable
|
|
||||||
|
|
||||||
buildCommand(ctx, msg, &early, os.Stderr).MustParse(os.Args[1:], func(err error) {
|
|
||||||
msg.Verbosef("command returned %v", err)
|
|
||||||
if errors.Is(err, errSuccess) {
|
if errors.Is(err, errSuccess) {
|
||||||
msg.BeforeExit()
|
hlog.BeforeExit()
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
// this catches faulty command handlers that fail to return before this point
|
// this catches faulty command handlers that fail to return before this point
|
||||||
|
|||||||
+48
-121
@@ -1,7 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
@@ -11,104 +11,66 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
"hakurei.app/internal/outcome"
|
"hakurei.app/internal/app/state"
|
||||||
"hakurei.app/internal/store"
|
"hakurei.app/internal/hlog"
|
||||||
"hakurei.app/message"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// tryPath attempts to read [hst.Config] from multiple sources.
|
func tryPath(name string) (config *hst.Config) {
|
||||||
//
|
var r io.Reader
|
||||||
// tryPath reads from [os.Stdin] if name has value "-". Otherwise, name is
|
|
||||||
// passed to tryFd, and if that returns nil, name is passed to [os.Open].
|
|
||||||
func tryPath(msg message.Msg, name string) (config *hst.Config) {
|
|
||||||
var r io.ReadCloser
|
|
||||||
config = new(hst.Config)
|
config = new(hst.Config)
|
||||||
|
|
||||||
if name != "-" {
|
if name != "-" {
|
||||||
r = tryFd(msg, name)
|
r = tryFd(name)
|
||||||
if r == nil {
|
if r == nil {
|
||||||
msg.Verbose("load configuration from file")
|
hlog.Verbose("load configuration from file")
|
||||||
|
|
||||||
if f, err := os.Open(name); err != nil {
|
if f, err := os.Open(name); err != nil {
|
||||||
log.Fatal(err.Error())
|
log.Fatalf("cannot access configuration file %q: %s", name, err)
|
||||||
return
|
|
||||||
} else {
|
} else {
|
||||||
|
// finalizer closes f
|
||||||
r = f
|
r = f
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
defer func() {
|
||||||
|
if err := r.(io.ReadCloser).Close(); err != nil {
|
||||||
|
log.Printf("cannot close config fd: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
r = os.Stdin
|
r = os.Stdin
|
||||||
}
|
}
|
||||||
|
|
||||||
decodeJSON(log.Fatal, "load configuration", r, &config)
|
if err := json.NewDecoder(r).Decode(&config); err != nil {
|
||||||
if err := r.Close(); err != nil {
|
log.Fatalf("cannot load configuration: %v", err)
|
||||||
log.Fatal(err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// tryFd returns a [io.ReadCloser] if name represents an integer corresponding
|
func tryFd(name string) io.ReadCloser {
|
||||||
// to a valid file descriptor.
|
|
||||||
func tryFd(msg message.Msg, name string) io.ReadCloser {
|
|
||||||
if v, err := strconv.Atoi(name); err != nil {
|
if v, err := strconv.Atoi(name); err != nil {
|
||||||
if !errors.Is(err, strconv.ErrSyntax) {
|
if !errors.Is(err, strconv.ErrSyntax) {
|
||||||
msg.Verbosef("name cannot be interpreted as int64: %v", err)
|
hlog.Verbosef("name cannot be interpreted as int64: %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
if v < 3 { // reject standard streams
|
hlog.Verbosef("trying config stream from %d", v)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
msg.Verbosef("trying config stream from %d", v)
|
|
||||||
fd := uintptr(v)
|
fd := uintptr(v)
|
||||||
if _, _, errno := syscall.Syscall(
|
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
|
||||||
syscall.SYS_FCNTL,
|
if errors.Is(errno, syscall.EBADF) {
|
||||||
fd,
|
|
||||||
syscall.F_GETFD,
|
|
||||||
0,
|
|
||||||
); errno != 0 {
|
|
||||||
if errors.Is(errno, syscall.EBADF) { // reject bad fd
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
log.Fatalf("cannot get fd %d: %v", fd, errno)
|
log.Fatalf("cannot get fd %d: %v", fd, errno)
|
||||||
}
|
}
|
||||||
|
|
||||||
if outcome.IsPollDescriptor(fd) { // reject runtime internals
|
|
||||||
log.Fatalf("invalid config stream %d", fd)
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.NewFile(fd, strconv.Itoa(v))
|
return os.NewFile(fd, strconv.Itoa(v))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// shortLengthMin is the minimum length a short form identifier can have and
|
func tryShort(name string) (config *hst.Config, entry *state.State) {
|
||||||
// still be interpreted as an identifier.
|
likePrefix := false
|
||||||
const shortLengthMin = 1 << 3
|
if len(name) <= 32 {
|
||||||
|
likePrefix = true
|
||||||
// shortIdentifier returns an eight character short representation of [hst.ID]
|
|
||||||
// from its random bytes.
|
|
||||||
func shortIdentifier(id *hst.ID) string {
|
|
||||||
return shortIdentifierString(id.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// shortIdentifierString implements shortIdentifier on an arbitrary string.
|
|
||||||
func shortIdentifierString(s string) string {
|
|
||||||
return s[len(hst.ID{}) : len(hst.ID{})+shortLengthMin]
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryIdentifier attempts to match [hst.State] from a [hex] representation of
|
|
||||||
// [hst.ID] or a prefix of its lower half.
|
|
||||||
func tryIdentifier(msg message.Msg, name string, s *store.Store) *hst.State {
|
|
||||||
const (
|
|
||||||
likeShort = 1 << iota
|
|
||||||
likeFull
|
|
||||||
)
|
|
||||||
|
|
||||||
var likely uintptr
|
|
||||||
// half the hex representation
|
|
||||||
if len(name) >= shortLengthMin && len(name) <= len(hst.ID{}) {
|
|
||||||
// cannot safely decode here due to unknown alignment
|
|
||||||
for _, c := range name {
|
for _, c := range name {
|
||||||
if c >= '0' && c <= '9' {
|
if c >= '0' && c <= '9' {
|
||||||
continue
|
continue
|
||||||
@@ -116,68 +78,33 @@ func tryIdentifier(msg message.Msg, name string, s *store.Store) *hst.State {
|
|||||||
if c >= 'a' && c <= 'f' {
|
if c >= 'a' && c <= 'f' {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return nil
|
likePrefix = false
|
||||||
|
break
|
||||||
}
|
}
|
||||||
likely |= likeShort
|
|
||||||
} else if len(name) == hex.EncodedLen(len(hst.ID{})) {
|
|
||||||
likely |= likeFull
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if likely == 0 {
|
// try to match from state store
|
||||||
return nil
|
if likePrefix && len(name) >= 8 {
|
||||||
}
|
hlog.Verbose("argument looks like prefix")
|
||||||
|
|
||||||
entries, copyError := s.All()
|
s := state.NewMulti(std.Paths().RunDirPath.String())
|
||||||
defer func() {
|
if entries, err := state.Join(s); err != nil {
|
||||||
if err := copyError(); err != nil {
|
log.Printf("cannot join store: %v", err)
|
||||||
msg.GetLogger().Println(getMessage("cannot iterate over store:", err))
|
// drop to fetch from file
|
||||||
}
|
} else {
|
||||||
}()
|
for id := range entries {
|
||||||
|
v := id.String()
|
||||||
switch {
|
if strings.HasPrefix(v, name) {
|
||||||
case likely&likeShort != 0:
|
// match, use config from this state entry
|
||||||
msg.Verbose("argument looks like short identifier")
|
entry = entries[id]
|
||||||
for eh := range entries {
|
config = entry.Config
|
||||||
if eh.DecodeErr != nil {
|
break
|
||||||
msg.Verbose(getMessage("skipping instance:", eh.DecodeErr))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(eh.ID.String()[len(hst.ID{}):], name) {
|
|
||||||
var entry hst.State
|
|
||||||
if _, err := eh.Load(&entry); err != nil {
|
|
||||||
msg.GetLogger().Println(getMessage("cannot load state entry:", err))
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
return &entry
|
|
||||||
|
hlog.Verbosef("instance %s skipped", v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
|
||||||
case likely&likeFull != 0:
|
|
||||||
var likelyID hst.ID
|
|
||||||
if likelyID.UnmarshalText([]byte(name)) != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
msg.Verbose("argument looks like identifier")
|
|
||||||
for eh := range entries {
|
|
||||||
if eh.DecodeErr != nil {
|
|
||||||
msg.Verbose(getMessage("skipping instance:", eh.DecodeErr))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if eh.ID == likelyID {
|
|
||||||
var entry hst.State
|
|
||||||
if _, err := eh.Load(&entry); err != nil {
|
|
||||||
msg.GetLogger().Println(getMessage("cannot load state entry:", err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return &entry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
panic("unreachable")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
"hakurei.app/hst"
|
|
||||||
"hakurei.app/internal/store"
|
|
||||||
"hakurei.app/message"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestShortIdentifier(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
id := hst.ID{
|
|
||||||
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
|
|
||||||
0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10,
|
|
||||||
}
|
|
||||||
|
|
||||||
const want = "fedcba98"
|
|
||||||
if got := shortIdentifier(&id); got != want {
|
|
||||||
t.Errorf("shortIdentifier: %q, want %q", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTryIdentifier(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
msg := message.New(nil)
|
|
||||||
id := hst.ID{
|
|
||||||
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
|
|
||||||
0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10,
|
|
||||||
}
|
|
||||||
withBase := func(extra ...hst.State) []hst.State {
|
|
||||||
return append([]hst.State{
|
|
||||||
{ID: (hst.ID)(bytes.Repeat([]byte{0xaa}, len(hst.ID{}))), PID: 0xbeef, ShimPID: 0xcafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef0)},
|
|
||||||
{ID: (hst.ID)(bytes.Repeat([]byte{0xab}, len(hst.ID{}))), PID: 0x1beef, ShimPID: 0x1cafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef1)},
|
|
||||||
{ID: (hst.ID)(bytes.Repeat([]byte{0xf0}, len(hst.ID{}))), PID: 0x2beef, ShimPID: 0x2cafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef2)},
|
|
||||||
|
|
||||||
{ID: (hst.ID)(bytes.Repeat([]byte{0xfe}, len(hst.ID{}))), PID: 0xbed, ShimPID: 0xfff, Config: func() *hst.Config {
|
|
||||||
template := hst.Template()
|
|
||||||
template.Identity = hst.IdentityEnd
|
|
||||||
return template
|
|
||||||
}(), Time: time.Unix(0, 0xcafebabe0)},
|
|
||||||
{ID: (hst.ID)(bytes.Repeat([]byte{0xfc}, len(hst.ID{}))), PID: 0x1bed, ShimPID: 0x1fff, Config: func() *hst.Config {
|
|
||||||
template := hst.Template()
|
|
||||||
template.Identity = 0xfc
|
|
||||||
return template
|
|
||||||
}(), Time: time.Unix(0, 0xcafebabe1)},
|
|
||||||
{ID: (hst.ID)(bytes.Repeat([]byte{0xce}, len(hst.ID{}))), PID: 0x2bed, ShimPID: 0x2fff, Config: func() *hst.Config {
|
|
||||||
template := hst.Template()
|
|
||||||
template.Identity = 0xce
|
|
||||||
return template
|
|
||||||
}(), Time: time.Unix(0, 0xcafebabe2)},
|
|
||||||
}, extra...)
|
|
||||||
}
|
|
||||||
sampleEntry := hst.State{
|
|
||||||
ID: id,
|
|
||||||
PID: 0xcafe,
|
|
||||||
ShimPID: 0xdead,
|
|
||||||
Config: hst.Template(),
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
s string
|
|
||||||
data []hst.State
|
|
||||||
want *hst.State
|
|
||||||
}{
|
|
||||||
{"likely entries fault", "ffffffff", nil, nil},
|
|
||||||
|
|
||||||
{"likely short too short", "ff", nil, nil},
|
|
||||||
{"likely short too long", "fffffffffffffffff", nil, nil},
|
|
||||||
{"likely short invalid lower", "fffffff\x00", nil, nil},
|
|
||||||
{"likely short invalid higher", "0000000\xff", nil, nil},
|
|
||||||
{"short no match", "fedcba98", withBase(), nil},
|
|
||||||
{"short match", "fedcba98", withBase(sampleEntry), &sampleEntry},
|
|
||||||
{"short match single", "fedcba98", []hst.State{sampleEntry}, &sampleEntry},
|
|
||||||
{"short match longer", "fedcba98765", withBase(sampleEntry), &sampleEntry},
|
|
||||||
|
|
||||||
{"likely long invalid", "0123456789abcdeffedcba987654321\x00", nil, nil},
|
|
||||||
{"long no match", "0123456789abcdeffedcba9876543210", withBase(), nil},
|
|
||||||
{"long match", "0123456789abcdeffedcba9876543210", withBase(sampleEntry), &sampleEntry},
|
|
||||||
{"long match single", "0123456789abcdeffedcba9876543210", []hst.State{sampleEntry}, &sampleEntry},
|
|
||||||
}
|
|
||||||
for _, tc := range testCases {
|
|
||||||
base := check.MustAbs(t.TempDir()).Append("store")
|
|
||||||
s := store.New(base)
|
|
||||||
for i := range tc.data {
|
|
||||||
if h, err := s.Handle(tc.data[i].Identity); err != nil {
|
|
||||||
t.Fatalf("Handle: error = %v", err)
|
|
||||||
} else {
|
|
||||||
var unlock func()
|
|
||||||
if unlock, err = h.Lock(); err != nil {
|
|
||||||
t.Fatalf("Lock: error = %v", err)
|
|
||||||
}
|
|
||||||
_, err = h.Save(&tc.data[i])
|
|
||||||
unlock()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Save: error = %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// store must not be written to beyond this point
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
got := tryIdentifier(msg, tc.s, store.New(base))
|
|
||||||
if !reflect.DeepEqual(got, tc.want) {
|
|
||||||
t.Errorf("tryIdentifier: %#v, want %#v", got, tc.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+132
-121
@@ -1,10 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -12,43 +13,46 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
"hakurei.app/internal/outcome"
|
"hakurei.app/internal/app/state"
|
||||||
"hakurei.app/internal/store"
|
"hakurei.app/internal/hlog"
|
||||||
"hakurei.app/message"
|
"hakurei.app/system/dbus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// printShowSystem populates and writes a representation of [hst.Info] to output.
|
|
||||||
func printShowSystem(output io.Writer, short, flagJSON bool) {
|
func printShowSystem(output io.Writer, short, flagJSON bool) {
|
||||||
t := newPrinter(output)
|
t := newPrinter(output)
|
||||||
defer t.MustFlush()
|
defer t.MustFlush()
|
||||||
hi := outcome.Info()
|
|
||||||
|
info := &hst.Info{Paths: std.Paths()}
|
||||||
|
|
||||||
|
// get hid by querying uid of identity 0
|
||||||
|
if uid, err := std.Uid(0); err != nil {
|
||||||
|
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
|
||||||
|
os.Exit(1)
|
||||||
|
} else {
|
||||||
|
info.User = (uid / 10000) - 100
|
||||||
|
}
|
||||||
|
|
||||||
if flagJSON {
|
if flagJSON {
|
||||||
encodeJSON(log.Fatal, output, short, hi)
|
printJSON(output, short, info)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Printf("Version:\t%s (libwayland %s)\n", hi.Version, hi.WaylandVersion)
|
t.Printf("User:\t%d\n", info.User)
|
||||||
t.Printf("User:\t%d\n", hi.User)
|
t.Printf("TempDir:\t%s\n", info.TempDir)
|
||||||
t.Printf("TempDir:\t%s\n", hi.TempDir)
|
t.Printf("SharePath:\t%s\n", info.SharePath)
|
||||||
t.Printf("SharePath:\t%s\n", hi.SharePath)
|
t.Printf("RuntimePath:\t%s\n", info.RuntimePath)
|
||||||
t.Printf("RuntimePath:\t%s\n", hi.RuntimePath)
|
t.Printf("RunDirPath:\t%s\n", info.RunDirPath)
|
||||||
t.Printf("RunDirPath:\t%s\n", hi.RunDirPath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// printShowInstance writes a representation of [hst.State] or [hst.Config] to output.
|
|
||||||
func printShowInstance(
|
func printShowInstance(
|
||||||
output io.Writer, now time.Time,
|
output io.Writer, now time.Time,
|
||||||
instance *hst.State, config *hst.Config,
|
instance *state.State, config *hst.Config,
|
||||||
short, flagJSON bool,
|
short, flagJSON bool) {
|
||||||
) (valid bool) {
|
|
||||||
valid = true
|
|
||||||
|
|
||||||
if flagJSON {
|
if flagJSON {
|
||||||
if instance != nil {
|
if instance != nil {
|
||||||
encodeJSON(log.Fatal, output, short, instance)
|
printJSON(output, short, instance)
|
||||||
} else {
|
} else {
|
||||||
encodeJSON(log.Fatal, output, short, config)
|
printJSON(output, short, config)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -56,21 +60,13 @@ func printShowInstance(
|
|||||||
t := newPrinter(output)
|
t := newPrinter(output)
|
||||||
defer t.MustFlush()
|
defer t.MustFlush()
|
||||||
|
|
||||||
if err := config.Validate(hst.VAllowInsecure); err != nil {
|
if config.Container == nil {
|
||||||
valid = false
|
mustPrint(output, "Warning: this configuration uses permissive defaults!\n\n")
|
||||||
if m, ok := message.GetMessage(err); ok {
|
|
||||||
mustPrint(output, "Error: "+m+"!\n\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if config == nil {
|
|
||||||
// nothing to print
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if instance != nil {
|
if instance != nil {
|
||||||
t.Printf("State\n")
|
t.Printf("State\n")
|
||||||
t.Printf(" Instance:\t%s (%d -> %d)\n", instance.ID.String(), instance.PID, instance.ShimPID)
|
t.Printf(" Instance:\t%s (%d)\n", instance.ID.String(), instance.PID)
|
||||||
t.Printf(" Uptime:\t%s\n", now.Sub(instance.Time).Round(time.Second).String())
|
t.Printf(" Uptime:\t%s\n", now.Sub(instance.Time).Round(time.Second).String())
|
||||||
t.Printf("\n")
|
t.Printf("\n")
|
||||||
}
|
}
|
||||||
@@ -85,34 +81,40 @@ func printShowInstance(
|
|||||||
if len(config.Groups) > 0 {
|
if len(config.Groups) > 0 {
|
||||||
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
|
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
|
||||||
}
|
}
|
||||||
|
if config.Home != nil {
|
||||||
|
t.Printf(" Home:\t%s\n", config.Home)
|
||||||
|
}
|
||||||
if config.Container != nil {
|
if config.Container != nil {
|
||||||
flags := config.Container.Flags.String()
|
params := config.Container
|
||||||
|
if params.Hostname != "" {
|
||||||
// this is included in the upper hst.Config struct but is relevant here
|
t.Printf(" Hostname:\t%s\n", params.Hostname)
|
||||||
const flagDirectWayland = "directwl"
|
}
|
||||||
if config.DirectWayland {
|
flags := make([]string, 0, 7)
|
||||||
// hardcoded value when every flag is unset
|
writeFlag := func(name string, value bool) {
|
||||||
if flags == "none" {
|
if value {
|
||||||
flags = flagDirectWayland
|
flags = append(flags, name)
|
||||||
} else {
|
|
||||||
flags += ", " + flagDirectWayland
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.Printf(" Flags:\t%s\n", flags)
|
writeFlag("userns", params.Userns)
|
||||||
|
writeFlag("devel", params.Devel)
|
||||||
|
writeFlag("net", params.HostNet)
|
||||||
|
writeFlag("abstract", params.HostAbstract)
|
||||||
|
writeFlag("device", params.Device)
|
||||||
|
writeFlag("tty", params.Tty)
|
||||||
|
writeFlag("mapuid", params.MapRealUID)
|
||||||
|
writeFlag("directwl", config.DirectWayland)
|
||||||
|
if len(flags) == 0 {
|
||||||
|
flags = append(flags, "none")
|
||||||
|
}
|
||||||
|
t.Printf(" Flags:\t%s\n", strings.Join(flags, " "))
|
||||||
|
|
||||||
if config.Container.Home != nil {
|
if config.Path != nil {
|
||||||
t.Printf(" Home:\t%s\n", config.Container.Home)
|
t.Printf(" Path:\t%s\n", config.Path)
|
||||||
}
|
|
||||||
if config.Container.Hostname != "" {
|
|
||||||
t.Printf(" Hostname:\t%s\n", config.Container.Hostname)
|
|
||||||
}
|
|
||||||
if config.Container.Path != nil {
|
|
||||||
t.Printf(" Path:\t%s\n", config.Container.Path)
|
|
||||||
}
|
|
||||||
if len(config.Container.Args) > 0 {
|
|
||||||
t.Printf(" Arguments:\t%s\n", strings.Join(config.Container.Args, " "))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(config.Args) > 0 {
|
||||||
|
t.Printf(" Arguments:\t%s\n", strings.Join(config.Args, " "))
|
||||||
|
}
|
||||||
t.Printf("\n")
|
t.Printf("\n")
|
||||||
|
|
||||||
if !short {
|
if !short {
|
||||||
@@ -120,7 +122,6 @@ func printShowInstance(
|
|||||||
t.Printf("Filesystem\n")
|
t.Printf("Filesystem\n")
|
||||||
for _, f := range config.Container.Filesystem {
|
for _, f := range config.Container.Filesystem {
|
||||||
if !f.Valid() {
|
if !f.Valid() {
|
||||||
valid = false
|
|
||||||
t.Println(" <invalid>")
|
t.Println(" <invalid>")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -130,14 +131,17 @@ func printShowInstance(
|
|||||||
}
|
}
|
||||||
if len(config.ExtraPerms) > 0 {
|
if len(config.ExtraPerms) > 0 {
|
||||||
t.Printf("Extra ACL\n")
|
t.Printf("Extra ACL\n")
|
||||||
for i := range config.ExtraPerms {
|
for _, p := range config.ExtraPerms {
|
||||||
t.Printf(" %s\n", config.ExtraPerms[i].String())
|
if p == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.Printf(" %s\n", p.String())
|
||||||
}
|
}
|
||||||
t.Printf("\n")
|
t.Printf("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
printDBus := func(c *hst.BusConfig) {
|
printDBus := func(c *dbus.Config) {
|
||||||
t.Printf(" Filter:\t%v\n", c.Filter)
|
t.Printf(" Filter:\t%v\n", c.Filter)
|
||||||
if len(c.See) > 0 {
|
if len(c.See) > 0 {
|
||||||
t.Printf(" See:\t%q\n", c.See)
|
t.Printf(" See:\t%q\n", c.See)
|
||||||
@@ -165,57 +169,59 @@ func printShowInstance(
|
|||||||
printDBus(config.SystemBus)
|
printDBus(config.SystemBus)
|
||||||
t.Printf("\n")
|
t.Printf("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// printPs writes a representation of active instances to output.
|
func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON bool) {
|
||||||
func printPs(msg message.Msg, output io.Writer, now time.Time, s *store.Store, short, flagJSON bool) {
|
var entries state.Entries
|
||||||
f := func(a func(eh *store.EntryHandle)) {
|
if e, err := state.Join(s); err != nil {
|
||||||
entries, copyError := s.All()
|
log.Fatalf("cannot join store: %v", err)
|
||||||
for eh := range entries {
|
} else {
|
||||||
a(eh)
|
entries = e
|
||||||
}
|
}
|
||||||
if err := copyError(); err != nil {
|
if err := s.Close(); err != nil {
|
||||||
msg.GetLogger().Println(getMessage("cannot iterate over store:", err))
|
log.Printf("cannot close store: %v", err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if short { // short output requires identifier only
|
if !short && flagJSON {
|
||||||
var identifiers []*hst.ID
|
es := make(map[string]*state.State, len(entries))
|
||||||
f(func(eh *store.EntryHandle) {
|
for id, instance := range entries {
|
||||||
if _, err := eh.Load(nil); err != nil { // passes through decode error
|
es[id.String()] = instance
|
||||||
msg.GetLogger().Println(getMessage("cannot validate state entry header:", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
identifiers = append(identifiers, &eh.ID)
|
|
||||||
})
|
|
||||||
slices.SortFunc(identifiers, func(a, b *hst.ID) int { return bytes.Compare(a[:], b[:]) })
|
|
||||||
|
|
||||||
if flagJSON {
|
|
||||||
encodeJSON(log.Fatal, output, short, identifiers)
|
|
||||||
} else {
|
|
||||||
for _, id := range identifiers {
|
|
||||||
mustPrintln(output, shortIdentifier(id))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
printJSON(output, short, es)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// long output requires full instance state
|
// sort state entries by id string to ensure consistency between runs
|
||||||
var instances []*hst.State
|
exp := make([]*expandedStateEntry, 0, len(entries))
|
||||||
f(func(eh *store.EntryHandle) {
|
for id, instance := range entries {
|
||||||
var state hst.State
|
// gracefully skip nil states
|
||||||
if _, err := eh.Load(&state); err != nil { // passes through decode error
|
if instance == nil {
|
||||||
msg.GetLogger().Println(getMessage("cannot load state entry:", err))
|
log.Printf("got invalid state entry %s", id.String())
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
instances = append(instances, &state)
|
|
||||||
})
|
|
||||||
slices.SortFunc(instances, func(a, b *hst.State) int { return bytes.Compare(a.ID[:], b.ID[:]) })
|
|
||||||
|
|
||||||
if flagJSON {
|
// gracefully skip inconsistent states
|
||||||
encodeJSON(log.Fatal, output, short, instances)
|
if id != instance.ID {
|
||||||
|
log.Printf("possible store corruption: entry %s has id %s",
|
||||||
|
id.String(), instance.ID.String())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
exp = append(exp, &expandedStateEntry{s: id.String(), State: instance})
|
||||||
|
}
|
||||||
|
slices.SortFunc(exp, func(a, b *expandedStateEntry) int { return a.Time.Compare(b.Time) })
|
||||||
|
|
||||||
|
if short {
|
||||||
|
if flagJSON {
|
||||||
|
v := make([]string, len(exp))
|
||||||
|
for i, e := range exp {
|
||||||
|
v[i] = e.s
|
||||||
|
}
|
||||||
|
printJSON(output, short, v)
|
||||||
|
} else {
|
||||||
|
for _, e := range exp {
|
||||||
|
mustPrintln(output, e.s[:8])
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,48 +229,61 @@ func printPs(msg message.Msg, output io.Writer, now time.Time, s *store.Store, s
|
|||||||
defer t.MustFlush()
|
defer t.MustFlush()
|
||||||
|
|
||||||
t.Println("\tInstance\tPID\tApplication\tUptime")
|
t.Println("\tInstance\tPID\tApplication\tUptime")
|
||||||
for _, instance := range instances {
|
for _, e := range exp {
|
||||||
|
if len(e.s) != 1<<5 {
|
||||||
|
// unreachable
|
||||||
|
log.Printf("possible store corruption: invalid instance string %s", e.s)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
as := "(No configuration information)"
|
as := "(No configuration information)"
|
||||||
if instance.Config != nil {
|
if e.Config != nil {
|
||||||
as = strconv.Itoa(instance.Config.Identity)
|
as = strconv.Itoa(e.Config.Identity)
|
||||||
id := instance.Config.ID
|
id := e.Config.ID
|
||||||
if id == "" {
|
if id == "" {
|
||||||
id = "app.hakurei." + shortIdentifier(&instance.ID)
|
id = "app.hakurei." + e.s[:8]
|
||||||
}
|
}
|
||||||
as += " (" + id + ")"
|
as += " (" + id + ")"
|
||||||
}
|
}
|
||||||
t.Printf("\t%s\t%d\t%s\t%s\n",
|
t.Printf("\t%s\t%d\t%s\t%s\n",
|
||||||
shortIdentifier(&instance.ID), instance.PID, as, now.Sub(instance.Time).Round(time.Second).String())
|
e.s[:8], e.PID, as, now.Sub(e.Time).Round(time.Second).String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type expandedStateEntry struct {
|
||||||
|
s string
|
||||||
|
*state.State
|
||||||
|
}
|
||||||
|
|
||||||
|
func printJSON(output io.Writer, short bool, v any) {
|
||||||
|
encoder := json.NewEncoder(output)
|
||||||
|
if !short {
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
}
|
||||||
|
if err := encoder.Encode(v); err != nil {
|
||||||
|
log.Fatalf("cannot serialise: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newPrinter returns a configured, wrapped [tabwriter.Writer].
|
|
||||||
func newPrinter(output io.Writer) *tp { return &tp{tabwriter.NewWriter(output, 0, 1, 4, ' ', 0)} }
|
func newPrinter(output io.Writer) *tp { return &tp{tabwriter.NewWriter(output, 0, 1, 4, ' ', 0)} }
|
||||||
|
|
||||||
// tp wraps [tabwriter.Writer] to provide additional formatting methods.
|
|
||||||
type tp struct{ *tabwriter.Writer }
|
type tp struct{ *tabwriter.Writer }
|
||||||
|
|
||||||
// Printf calls [fmt.Fprintf] on the underlying [tabwriter.Writer].
|
|
||||||
func (p *tp) Printf(format string, a ...any) {
|
func (p *tp) Printf(format string, a ...any) {
|
||||||
if _, err := fmt.Fprintf(p, format, a...); err != nil {
|
if _, err := fmt.Fprintf(p, format, a...); err != nil {
|
||||||
log.Fatalf("cannot write to tabwriter: %v", err)
|
log.Fatalf("cannot write to tabwriter: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Println calls [fmt.Fprintln] on the underlying [tabwriter.Writer].
|
|
||||||
func (p *tp) Println(a ...any) {
|
func (p *tp) Println(a ...any) {
|
||||||
if _, err := fmt.Fprintln(p, a...); err != nil {
|
if _, err := fmt.Fprintln(p, a...); err != nil {
|
||||||
log.Fatalf("cannot write to tabwriter: %v", err)
|
log.Fatalf("cannot write to tabwriter: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustFlush calls the Flush method of [tabwriter.Writer] and calls [log.Fatalf] on a non-nil error.
|
|
||||||
func (p *tp) MustFlush() {
|
func (p *tp) MustFlush() {
|
||||||
if err := p.Writer.Flush(); err != nil {
|
if err := p.Writer.Flush(); err != nil {
|
||||||
log.Fatalf("cannot flush tabwriter: %v", err)
|
log.Fatalf("cannot flush tabwriter: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustPrint(output io.Writer, a ...any) {
|
func mustPrint(output io.Writer, a ...any) {
|
||||||
if _, err := fmt.Fprint(output, a...); err != nil {
|
if _, err := fmt.Fprint(output, a...); err != nil {
|
||||||
log.Fatalf("cannot print: %v", err)
|
log.Fatalf("cannot print: %v", err)
|
||||||
@@ -275,11 +294,3 @@ func mustPrintln(output io.Writer, a ...any) {
|
|||||||
log.Fatalf("cannot print: %v", err)
|
log.Fatalf("cannot print: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getMessage returns a [message.Error] message if available, or err prefixed with fallback otherwise.
|
|
||||||
func getMessage(fallback string, err error) string {
|
|
||||||
if m, ok := message.GetMessage(err); ok {
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
return fmt.Sprintln(fallback, err)
|
|
||||||
}
|
|
||||||
|
|||||||
+461
-506
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
|||||||
|
This program is a proof of concept and is now deprecated. It is only kept
|
||||||
|
around for API demonstration purposes and to make the most out of the test
|
||||||
|
suite.
|
||||||
|
|
||||||
|
This program is replaced by planterette, which can be found at
|
||||||
|
https://git.gensokyo.uk/security/planterette. Development effort should be
|
||||||
|
focused there instead.
|
||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"hakurei.app/container"
|
||||||
|
"hakurei.app/container/seccomp"
|
||||||
|
"hakurei.app/hst"
|
||||||
|
"hakurei.app/system/dbus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type appInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
|
||||||
|
// passed through to [hst.Config]
|
||||||
|
ID string `json:"id"`
|
||||||
|
// passed through to [hst.Config]
|
||||||
|
Identity int `json:"identity"`
|
||||||
|
// passed through to [hst.Config]
|
||||||
|
Groups []string `json:"groups,omitempty"`
|
||||||
|
// passed through to [hst.Config]
|
||||||
|
Devel bool `json:"devel,omitempty"`
|
||||||
|
// passed through to [hst.Config]
|
||||||
|
Userns bool `json:"userns,omitempty"`
|
||||||
|
// passed through to [hst.Config]
|
||||||
|
HostNet bool `json:"net,omitempty"`
|
||||||
|
// passed through to [hst.Config]
|
||||||
|
HostAbstract bool `json:"abstract,omitempty"`
|
||||||
|
// passed through to [hst.Config]
|
||||||
|
Device bool `json:"dev,omitempty"`
|
||||||
|
// passed through to [hst.Config]
|
||||||
|
Tty bool `json:"tty,omitempty"`
|
||||||
|
// passed through to [hst.Config]
|
||||||
|
MapRealUID bool `json:"map_real_uid,omitempty"`
|
||||||
|
// passed through to [hst.Config]
|
||||||
|
DirectWayland bool `json:"direct_wayland,omitempty"`
|
||||||
|
// passed through to [hst.Config]
|
||||||
|
SystemBus *dbus.Config `json:"system_bus,omitempty"`
|
||||||
|
// passed through to [hst.Config]
|
||||||
|
SessionBus *dbus.Config `json:"session_bus,omitempty"`
|
||||||
|
// passed through to [hst.Config]
|
||||||
|
Enablements *hst.Enablements `json:"enablements,omitempty"`
|
||||||
|
|
||||||
|
// passed through to [hst.Config]
|
||||||
|
Multiarch bool `json:"multiarch,omitempty"`
|
||||||
|
// passed through to [hst.Config]
|
||||||
|
Bluetooth bool `json:"bluetooth,omitempty"`
|
||||||
|
|
||||||
|
// allow gpu access within sandbox
|
||||||
|
GPU bool `json:"gpu"`
|
||||||
|
// store path to nixGL mesa wrappers
|
||||||
|
Mesa string `json:"mesa,omitempty"`
|
||||||
|
// store path to nixGL source
|
||||||
|
NixGL string `json:"nix_gl,omitempty"`
|
||||||
|
// store path to activate-and-exec script
|
||||||
|
Launcher *container.Absolute `json:"launcher"`
|
||||||
|
// store path to /run/current-system
|
||||||
|
CurrentSystem *container.Absolute `json:"current_system"`
|
||||||
|
// store path to home-manager activation package
|
||||||
|
ActivationPackage string `json:"activation_package"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *appInfo) toHst(pathSet *appPathSet, pathname *container.Absolute, argv []string, flagDropShell bool) *hst.Config {
|
||||||
|
config := &hst.Config{
|
||||||
|
ID: app.ID,
|
||||||
|
|
||||||
|
Path: pathname,
|
||||||
|
Args: argv,
|
||||||
|
|
||||||
|
Enablements: app.Enablements,
|
||||||
|
|
||||||
|
SystemBus: app.SystemBus,
|
||||||
|
SessionBus: app.SessionBus,
|
||||||
|
DirectWayland: app.DirectWayland,
|
||||||
|
|
||||||
|
Username: "hakurei",
|
||||||
|
Shell: pathShell,
|
||||||
|
Home: pathDataData.Append(app.ID),
|
||||||
|
|
||||||
|
Identity: app.Identity,
|
||||||
|
Groups: app.Groups,
|
||||||
|
|
||||||
|
Container: &hst.ContainerConfig{
|
||||||
|
Hostname: formatHostname(app.Name),
|
||||||
|
Devel: app.Devel,
|
||||||
|
Userns: app.Userns,
|
||||||
|
HostNet: app.HostNet,
|
||||||
|
HostAbstract: app.HostAbstract,
|
||||||
|
Device: app.Device,
|
||||||
|
Tty: app.Tty || flagDropShell,
|
||||||
|
MapRealUID: app.MapRealUID,
|
||||||
|
Filesystem: []hst.FilesystemConfigJSON{
|
||||||
|
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append("store"), Target: pathNixStore}},
|
||||||
|
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
|
||||||
|
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
|
||||||
|
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: pathSet.metaPath, Target: hst.AbsTmp.Append("app")}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSEtc.Append("resolv.conf"), Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("block"), Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("bus"), Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("class"), Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("dev"), Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("devices"), Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExtraPerms: []*hst.ExtraPermConfig{
|
||||||
|
{Path: dataHome, Execute: true},
|
||||||
|
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if app.Multiarch {
|
||||||
|
config.Container.SeccompFlags |= seccomp.AllowMultiarch
|
||||||
|
}
|
||||||
|
if app.Bluetooth {
|
||||||
|
config.Container.SeccompFlags |= seccomp.AllowBluetooth
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadAppInfo(name string, beforeFail func()) *appInfo {
|
||||||
|
bundle := new(appInfo)
|
||||||
|
if f, err := os.Open(name); err != nil {
|
||||||
|
beforeFail()
|
||||||
|
log.Fatalf("cannot open bundle: %v", err)
|
||||||
|
} else if err = json.NewDecoder(f).Decode(&bundle); err != nil {
|
||||||
|
beforeFail()
|
||||||
|
log.Fatalf("cannot parse bundle metadata: %v", err)
|
||||||
|
} else if err = f.Close(); err != nil {
|
||||||
|
log.Printf("cannot close bundle metadata: %v", err)
|
||||||
|
// not fatal
|
||||||
|
}
|
||||||
|
|
||||||
|
if bundle.ID == "" {
|
||||||
|
beforeFail()
|
||||||
|
log.Fatal("application identifier must not be empty")
|
||||||
|
}
|
||||||
|
if bundle.Launcher == nil {
|
||||||
|
beforeFail()
|
||||||
|
log.Fatal("launcher must not be empty")
|
||||||
|
}
|
||||||
|
if bundle.CurrentSystem == nil {
|
||||||
|
beforeFail()
|
||||||
|
log.Fatal("current-system must not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatHostname(name string) string {
|
||||||
|
if h, err := os.Hostname(); err != nil {
|
||||||
|
log.Printf("cannot get hostname: %v", err)
|
||||||
|
return "hakurei-" + name
|
||||||
|
} else {
|
||||||
|
return h + "-" + name
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
{
|
||||||
|
nixpkgsFor,
|
||||||
|
system,
|
||||||
|
nixpkgs,
|
||||||
|
home-manager,
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
stdenv,
|
||||||
|
closureInfo,
|
||||||
|
writeScript,
|
||||||
|
runtimeShell,
|
||||||
|
writeText,
|
||||||
|
symlinkJoin,
|
||||||
|
vmTools,
|
||||||
|
runCommand,
|
||||||
|
fetchFromGitHub,
|
||||||
|
|
||||||
|
zstd,
|
||||||
|
nix,
|
||||||
|
sqlite,
|
||||||
|
|
||||||
|
name ? throw "name is required",
|
||||||
|
version ? throw "version is required",
|
||||||
|
pname ? "${name}-${version}",
|
||||||
|
modules ? [ ],
|
||||||
|
nixosModules ? [ ],
|
||||||
|
script ? ''
|
||||||
|
exec "$SHELL" "$@"
|
||||||
|
'',
|
||||||
|
|
||||||
|
id ? name,
|
||||||
|
identity ? throw "identity is required",
|
||||||
|
groups ? [ ],
|
||||||
|
userns ? false,
|
||||||
|
net ? true,
|
||||||
|
dev ? false,
|
||||||
|
no_new_session ? false,
|
||||||
|
map_real_uid ? false,
|
||||||
|
direct_wayland ? false,
|
||||||
|
system_bus ? null,
|
||||||
|
session_bus ? null,
|
||||||
|
|
||||||
|
allow_wayland ? true,
|
||||||
|
allow_x11 ? false,
|
||||||
|
allow_dbus ? true,
|
||||||
|
allow_pulse ? true,
|
||||||
|
gpu ? allow_wayland || allow_x11,
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
inherit (lib) optionals;
|
||||||
|
|
||||||
|
homeManagerConfiguration = home-manager.lib.homeManagerConfiguration {
|
||||||
|
pkgs = nixpkgsFor.${system};
|
||||||
|
modules = modules ++ [
|
||||||
|
{
|
||||||
|
home = {
|
||||||
|
username = "hakurei";
|
||||||
|
homeDirectory = "/data/data/${id}";
|
||||||
|
stateVersion = "22.11";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
launcher = writeScript "hakurei-${pname}" ''
|
||||||
|
#!${runtimeShell} -el
|
||||||
|
${script}
|
||||||
|
'';
|
||||||
|
|
||||||
|
extraNixOSConfig =
|
||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
environment = {
|
||||||
|
etc.nixpkgs.source = nixpkgs.outPath;
|
||||||
|
systemPackages = [ pkgs.nix ];
|
||||||
|
};
|
||||||
|
|
||||||
|
imports = nixosModules;
|
||||||
|
};
|
||||||
|
nixos = nixpkgs.lib.nixosSystem {
|
||||||
|
inherit system;
|
||||||
|
modules = [
|
||||||
|
extraNixOSConfig
|
||||||
|
{ nix.settings.experimental-features = [ "flakes" ]; }
|
||||||
|
{ nix.settings.experimental-features = [ "nix-command" ]; }
|
||||||
|
{ boot.isContainer = true; }
|
||||||
|
{ system.stateVersion = "22.11"; }
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
etc = vmTools.runInLinuxVM (
|
||||||
|
runCommand "etc" { } ''
|
||||||
|
mkdir -p /etc
|
||||||
|
${nixos.config.system.build.etcActivationCommands}
|
||||||
|
|
||||||
|
# remove unused files
|
||||||
|
rm -rf /etc/sudoers
|
||||||
|
|
||||||
|
mkdir -p $out
|
||||||
|
tar -C /etc -cf "$out/etc.tar" .
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
|
extendSessionDefault = id: ext: {
|
||||||
|
filter = true;
|
||||||
|
|
||||||
|
talk = [ "org.freedesktop.Notifications" ] ++ ext.talk;
|
||||||
|
own =
|
||||||
|
(optionals (id != null) [
|
||||||
|
"${id}.*"
|
||||||
|
"org.mpris.MediaPlayer2.${id}.*"
|
||||||
|
])
|
||||||
|
++ ext.own;
|
||||||
|
|
||||||
|
inherit (ext) call broadcast;
|
||||||
|
};
|
||||||
|
|
||||||
|
nixGL = fetchFromGitHub {
|
||||||
|
owner = "nix-community";
|
||||||
|
repo = "nixGL";
|
||||||
|
rev = "310f8e49a149e4c9ea52f1adf70cdc768ec53f8a";
|
||||||
|
hash = "sha256-lnzZQYG0+EXl/6NkGpyIz+FEOc/DSEG57AP1VsdeNrM=";
|
||||||
|
};
|
||||||
|
|
||||||
|
mesaWrappers =
|
||||||
|
let
|
||||||
|
isIntelX86Platform = system == "x86_64-linux";
|
||||||
|
nixGLPackages = import (nixGL + "/default.nix") {
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
enable32bits = isIntelX86Platform;
|
||||||
|
enableIntelX86Extensions = isIntelX86Platform;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
symlinkJoin {
|
||||||
|
name = "nixGL-mesa";
|
||||||
|
paths = with nixGLPackages; [
|
||||||
|
nixGLIntel
|
||||||
|
nixVulkanIntel
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
info = builtins.toJSON {
|
||||||
|
inherit
|
||||||
|
name
|
||||||
|
version
|
||||||
|
id
|
||||||
|
identity
|
||||||
|
launcher
|
||||||
|
groups
|
||||||
|
userns
|
||||||
|
net
|
||||||
|
dev
|
||||||
|
no_new_session
|
||||||
|
map_real_uid
|
||||||
|
direct_wayland
|
||||||
|
system_bus
|
||||||
|
gpu
|
||||||
|
;
|
||||||
|
|
||||||
|
session_bus =
|
||||||
|
if session_bus != null then
|
||||||
|
(session_bus (extendSessionDefault id))
|
||||||
|
else
|
||||||
|
(extendSessionDefault id {
|
||||||
|
talk = [ ];
|
||||||
|
own = [ ];
|
||||||
|
call = { };
|
||||||
|
broadcast = { };
|
||||||
|
});
|
||||||
|
|
||||||
|
enablements = {
|
||||||
|
wayland = allow_wayland;
|
||||||
|
x11 = allow_x11;
|
||||||
|
dbus = allow_dbus;
|
||||||
|
pulse = allow_pulse;
|
||||||
|
};
|
||||||
|
|
||||||
|
mesa = if gpu then mesaWrappers else null;
|
||||||
|
nix_gl = if gpu then nixGL else null;
|
||||||
|
current_system = nixos.config.system.build.toplevel;
|
||||||
|
activation_package = homeManagerConfiguration.activationPackage;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
|
||||||
|
stdenv.mkDerivation {
|
||||||
|
name = "${pname}.pkg";
|
||||||
|
inherit version;
|
||||||
|
__structuredAttrs = true;
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
zstd
|
||||||
|
nix
|
||||||
|
sqlite
|
||||||
|
];
|
||||||
|
|
||||||
|
buildCommand = ''
|
||||||
|
NIX_ROOT="$(mktemp -d)"
|
||||||
|
export USER="nobody"
|
||||||
|
|
||||||
|
# create bootstrap store
|
||||||
|
bootstrapClosureInfo="${
|
||||||
|
closureInfo {
|
||||||
|
rootPaths = [
|
||||||
|
nix
|
||||||
|
nixos.config.system.build.toplevel
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
echo "copying bootstrap store paths..."
|
||||||
|
mkdir -p "$NIX_ROOT/nix/store"
|
||||||
|
xargs -n 1 -a "$bootstrapClosureInfo/store-paths" cp -at "$NIX_ROOT/nix/store/"
|
||||||
|
NIX_REMOTE="local?root=$NIX_ROOT" nix-store --load-db < "$bootstrapClosureInfo/registration"
|
||||||
|
NIX_REMOTE="local?root=$NIX_ROOT" nix-store --optimise
|
||||||
|
sqlite3 "$NIX_ROOT/nix/var/nix/db/db.sqlite" "UPDATE ValidPaths SET registrationTime = ''${SOURCE_DATE_EPOCH}"
|
||||||
|
chmod -R +r "$NIX_ROOT/nix/var"
|
||||||
|
|
||||||
|
# create binary cache
|
||||||
|
closureInfo="${
|
||||||
|
closureInfo {
|
||||||
|
rootPaths = [
|
||||||
|
homeManagerConfiguration.activationPackage
|
||||||
|
launcher
|
||||||
|
]
|
||||||
|
++ optionals gpu [
|
||||||
|
mesaWrappers
|
||||||
|
nixGL
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
echo "copying application paths..."
|
||||||
|
TMP_STORE="$(mktemp -d)"
|
||||||
|
mkdir -p "$TMP_STORE/nix/store"
|
||||||
|
xargs -n 1 -a "$closureInfo/store-paths" cp -at "$TMP_STORE/nix/store/"
|
||||||
|
NIX_REMOTE="local?root=$TMP_STORE" nix-store --load-db < "$closureInfo/registration"
|
||||||
|
sqlite3 "$TMP_STORE/nix/var/nix/db/db.sqlite" "UPDATE ValidPaths SET registrationTime = ''${SOURCE_DATE_EPOCH}"
|
||||||
|
NIX_REMOTE="local?root=$TMP_STORE" nix --offline --extra-experimental-features nix-command \
|
||||||
|
--verbose --log-format raw-with-logs \
|
||||||
|
copy --all --no-check-sigs --to \
|
||||||
|
"file://$NIX_ROOT/res?compression=zstd&compression-level=19¶llel-compression=true"
|
||||||
|
|
||||||
|
# package /etc
|
||||||
|
mkdir -p "$NIX_ROOT/etc"
|
||||||
|
tar -C "$NIX_ROOT/etc" -xf "${etc}/etc.tar"
|
||||||
|
|
||||||
|
# write metadata
|
||||||
|
cp "${writeText "bundle.json" info}" "$NIX_ROOT/bundle.json"
|
||||||
|
|
||||||
|
# create an intermediate file to improve zstd performance
|
||||||
|
INTER="$(mktemp)"
|
||||||
|
tar -C "$NIX_ROOT" -cf "$INTER" .
|
||||||
|
zstd -T0 -19 -fo "$out" "$INTER"
|
||||||
|
'';
|
||||||
|
}
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"hakurei.app/command"
|
||||||
|
"hakurei.app/container"
|
||||||
|
"hakurei.app/hst"
|
||||||
|
"hakurei.app/internal"
|
||||||
|
"hakurei.app/internal/hlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errSuccess = errors.New("success")
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
hlog.Prepare("hpkg")
|
||||||
|
if err := os.Setenv("SHELL", pathShell.String()); err != nil {
|
||||||
|
log.Fatalf("cannot set $SHELL: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if os.Geteuid() == 0 {
|
||||||
|
log.Fatal("this program must not run as root")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(),
|
||||||
|
syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer stop() // unreachable
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagVerbose bool
|
||||||
|
flagDropShell bool
|
||||||
|
)
|
||||||
|
c := command.New(os.Stderr, log.Printf, "hpkg", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
|
||||||
|
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
|
||||||
|
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action")
|
||||||
|
|
||||||
|
{
|
||||||
|
var (
|
||||||
|
flagDropShellActivate bool
|
||||||
|
)
|
||||||
|
c.NewCommand("install", "Install an application from its package", func(args []string) error {
|
||||||
|
if len(args) != 1 {
|
||||||
|
log.Println("invalid argument")
|
||||||
|
return syscall.EINVAL
|
||||||
|
}
|
||||||
|
pkgPath := args[0]
|
||||||
|
if !path.IsAbs(pkgPath) {
|
||||||
|
if dir, err := os.Getwd(); err != nil {
|
||||||
|
log.Printf("cannot get current directory: %v", err)
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
pkgPath = path.Join(dir, pkgPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Look up paths to programs started by hpkg.
|
||||||
|
This is done here to ease error handling as cleanup is not yet required.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ = lookPath("zstd")
|
||||||
|
tar = lookPath("tar")
|
||||||
|
chmod = lookPath("chmod")
|
||||||
|
rm = lookPath("rm")
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Extract package and set up for cleanup.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var workDir *container.Absolute
|
||||||
|
if p, err := os.MkdirTemp("", "hpkg.*"); err != nil {
|
||||||
|
log.Printf("cannot create temporary directory: %v", err)
|
||||||
|
return err
|
||||||
|
} else if workDir, err = container.NewAbs(p); err != nil {
|
||||||
|
log.Printf("invalid temporary directory: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cleanup := func() {
|
||||||
|
// should be faster than a native implementation
|
||||||
|
mustRun(chmod, "-R", "+w", workDir.String())
|
||||||
|
mustRun(rm, "-rf", workDir.String())
|
||||||
|
}
|
||||||
|
beforeRunFail.Store(&cleanup)
|
||||||
|
|
||||||
|
mustRun(tar, "-C", workDir.String(), "-xf", pkgPath)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Parse bundle and app metadata, do pre-install checks.
|
||||||
|
*/
|
||||||
|
|
||||||
|
bundle := loadAppInfo(path.Join(workDir.String(), "bundle.json"), cleanup)
|
||||||
|
pathSet := pathSetByApp(bundle.ID)
|
||||||
|
|
||||||
|
a := bundle
|
||||||
|
if s, err := os.Stat(pathSet.metaPath.String()); err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
cleanup()
|
||||||
|
log.Printf("cannot access %q: %v", pathSet.metaPath, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// did not modify app, clean installation condition met later
|
||||||
|
} else if s.IsDir() {
|
||||||
|
cleanup()
|
||||||
|
log.Printf("metadata path %q is not a file", pathSet.metaPath)
|
||||||
|
return syscall.EBADMSG
|
||||||
|
} else {
|
||||||
|
a = loadAppInfo(pathSet.metaPath.String(), cleanup)
|
||||||
|
if a.ID != bundle.ID {
|
||||||
|
cleanup()
|
||||||
|
log.Printf("app %q claims to have identifier %q",
|
||||||
|
bundle.ID, a.ID)
|
||||||
|
return syscall.EBADE
|
||||||
|
}
|
||||||
|
// sec: should verify credentials
|
||||||
|
}
|
||||||
|
|
||||||
|
if a != bundle {
|
||||||
|
// do not try to re-install
|
||||||
|
if a.NixGL == bundle.NixGL &&
|
||||||
|
a.CurrentSystem == bundle.CurrentSystem &&
|
||||||
|
a.Launcher == bundle.Launcher &&
|
||||||
|
a.ActivationPackage == bundle.ActivationPackage {
|
||||||
|
cleanup()
|
||||||
|
log.Printf("package %q is identical to local application %q",
|
||||||
|
pkgPath, a.ID)
|
||||||
|
return errSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
// identity determines uid
|
||||||
|
if a.Identity != bundle.Identity {
|
||||||
|
cleanup()
|
||||||
|
log.Printf("package %q identity %d differs from installed %d",
|
||||||
|
pkgPath, bundle.Identity, a.Identity)
|
||||||
|
return syscall.EBADE
|
||||||
|
}
|
||||||
|
|
||||||
|
// sec: should compare version string
|
||||||
|
hlog.Verbosef("installing application %q version %q over local %q",
|
||||||
|
bundle.ID, bundle.Version, a.Version)
|
||||||
|
} else {
|
||||||
|
hlog.Verbosef("application %q clean installation", bundle.ID)
|
||||||
|
// sec: should install credentials
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Setup steps for files owned by the target user.
|
||||||
|
*/
|
||||||
|
|
||||||
|
withCacheDir(ctx, "install", []string{
|
||||||
|
// export inner bundle path in the environment
|
||||||
|
"export BUNDLE=" + hst.Tmp + "/bundle",
|
||||||
|
// replace inner /etc
|
||||||
|
"mkdir -p etc",
|
||||||
|
"chmod -R +w etc",
|
||||||
|
"rm -rf etc",
|
||||||
|
"cp -dRf $BUNDLE/etc etc",
|
||||||
|
// replace inner /nix
|
||||||
|
"mkdir -p nix",
|
||||||
|
"chmod -R +w nix",
|
||||||
|
"rm -rf nix",
|
||||||
|
"cp -dRf /nix nix",
|
||||||
|
// copy from binary cache
|
||||||
|
"nix copy --offline --no-check-sigs --all --from file://$BUNDLE/res --to $PWD",
|
||||||
|
// deduplicate nix store
|
||||||
|
"nix store --offline --store $PWD optimise",
|
||||||
|
// make cache directory world-readable for autoetc
|
||||||
|
"chmod 0755 .",
|
||||||
|
}, workDir, bundle, pathSet, flagDropShell, cleanup)
|
||||||
|
|
||||||
|
if bundle.GPU {
|
||||||
|
withCacheDir(ctx, "mesa-wrappers", []string{
|
||||||
|
// link nixGL mesa wrappers
|
||||||
|
"mkdir -p nix/.nixGL",
|
||||||
|
"ln -s " + bundle.Mesa + "/bin/nixGLIntel nix/.nixGL/nixGL",
|
||||||
|
"ln -s " + bundle.Mesa + "/bin/nixVulkanIntel nix/.nixGL/nixVulkan",
|
||||||
|
}, workDir, bundle, pathSet, false, cleanup)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Activate home-manager generation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
withNixDaemon(ctx, "activate", []string{
|
||||||
|
// clean up broken links
|
||||||
|
"mkdir -p .local/state/{nix,home-manager}",
|
||||||
|
"chmod -R +w .local/state/{nix,home-manager}",
|
||||||
|
"rm -rf .local/state/{nix,home-manager}",
|
||||||
|
// run activation script
|
||||||
|
bundle.ActivationPackage + "/activate",
|
||||||
|
}, false, func(config *hst.Config) *hst.Config { return config },
|
||||||
|
bundle, pathSet, flagDropShellActivate, cleanup)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Installation complete. Write metadata to block re-installs or downgrades.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// serialise metadata to ensure consistency
|
||||||
|
if f, err := os.OpenFile(pathSet.metaPath.String()+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
|
||||||
|
cleanup()
|
||||||
|
log.Printf("cannot create metadata file: %v", err)
|
||||||
|
return err
|
||||||
|
} else if err = json.NewEncoder(f).Encode(bundle); err != nil {
|
||||||
|
cleanup()
|
||||||
|
log.Printf("cannot write metadata: %v", err)
|
||||||
|
return err
|
||||||
|
} else if err = f.Close(); err != nil {
|
||||||
|
log.Printf("cannot close metadata file: %v", err)
|
||||||
|
// not fatal
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Rename(pathSet.metaPath.String()+"~", pathSet.metaPath.String()); err != nil {
|
||||||
|
cleanup()
|
||||||
|
log.Printf("cannot rename metadata file: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup()
|
||||||
|
return errSuccess
|
||||||
|
}).
|
||||||
|
Flag(&flagDropShellActivate, "s", command.BoolFlag(false), "Drop to a shell on activation")
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var (
|
||||||
|
flagDropShellNixGL bool
|
||||||
|
flagAutoDrivers bool
|
||||||
|
)
|
||||||
|
c.NewCommand("start", "Start an application", func(args []string) error {
|
||||||
|
if len(args) < 1 {
|
||||||
|
log.Println("invalid argument")
|
||||||
|
return syscall.EINVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Parse app metadata.
|
||||||
|
*/
|
||||||
|
|
||||||
|
id := args[0]
|
||||||
|
pathSet := pathSetByApp(id)
|
||||||
|
a := loadAppInfo(pathSet.metaPath.String(), func() {})
|
||||||
|
if a.ID != id {
|
||||||
|
log.Printf("app %q claims to have identifier %q", id, a.ID)
|
||||||
|
return syscall.EBADE
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Prepare nixGL.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if a.GPU && flagAutoDrivers {
|
||||||
|
withNixDaemon(ctx, "nix-gl", []string{
|
||||||
|
"mkdir -p /nix/.nixGL/auto",
|
||||||
|
"rm -rf /nix/.nixGL/auto",
|
||||||
|
"export NIXPKGS_ALLOW_UNFREE=1",
|
||||||
|
"nix build --impure " +
|
||||||
|
"--out-link /nix/.nixGL/auto/opengl " +
|
||||||
|
"--override-input nixpkgs path:/etc/nixpkgs " +
|
||||||
|
"path:" + a.NixGL,
|
||||||
|
"nix build --impure " +
|
||||||
|
"--out-link /nix/.nixGL/auto/vulkan " +
|
||||||
|
"--override-input nixpkgs path:/etc/nixpkgs " +
|
||||||
|
"path:" + a.NixGL + "#nixVulkanNvidia",
|
||||||
|
}, true, func(config *hst.Config) *hst.Config {
|
||||||
|
config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfigJSON{
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSEtc.Append("resolv.conf"), Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("block"), Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("bus"), Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("class"), Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("dev"), Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("devices"), Optional: true}},
|
||||||
|
}...)
|
||||||
|
appendGPUFilesystem(config)
|
||||||
|
return config
|
||||||
|
}, a, pathSet, flagDropShellNixGL, func() {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create app configuration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pathname := a.Launcher
|
||||||
|
argv := make([]string, 1, len(args))
|
||||||
|
if flagDropShell {
|
||||||
|
pathname = pathShell
|
||||||
|
argv[0] = bash
|
||||||
|
} else {
|
||||||
|
argv[0] = a.Launcher.String()
|
||||||
|
}
|
||||||
|
argv = append(argv, args[1:]...)
|
||||||
|
config := a.toHst(pathSet, pathname, argv, flagDropShell)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Expose GPU devices.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if a.GPU {
|
||||||
|
config.Container.Filesystem = append(config.Container.Filesystem,
|
||||||
|
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append(".nixGL"), Target: hst.AbsTmp.Append("nixGL")}})
|
||||||
|
appendGPUFilesystem(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Spawn app.
|
||||||
|
*/
|
||||||
|
|
||||||
|
mustRunApp(ctx, config, func() {})
|
||||||
|
return errSuccess
|
||||||
|
}).
|
||||||
|
Flag(&flagDropShellNixGL, "s", command.BoolFlag(false), "Drop to a shell on nixGL build").
|
||||||
|
Flag(&flagAutoDrivers, "auto-drivers", command.BoolFlag(false), "Attempt automatic opengl driver detection")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.MustParse(os.Args[1:], func(err error) {
|
||||||
|
hlog.Verbosef("command returned %v", err)
|
||||||
|
if errors.Is(err, errSuccess) {
|
||||||
|
hlog.BeforeExit()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
log.Fatal("unreachable")
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"hakurei.app/container"
|
||||||
|
"hakurei.app/hst"
|
||||||
|
"hakurei.app/internal/hlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
const bash = "bash"
|
||||||
|
|
||||||
|
var (
|
||||||
|
dataHome *container.Absolute
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// dataHome
|
||||||
|
if a, err := container.NewAbs(os.Getenv("HAKUREI_DATA_HOME")); err == nil {
|
||||||
|
dataHome = a
|
||||||
|
} else {
|
||||||
|
dataHome = container.AbsFHSVarLib.Append("hakurei/" + strconv.Itoa(os.Getuid()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
pathBin = container.AbsFHSRoot.Append("bin")
|
||||||
|
|
||||||
|
pathNix = container.MustAbs("/nix/")
|
||||||
|
pathNixStore = pathNix.Append("store/")
|
||||||
|
pathCurrentSystem = container.AbsFHSRun.Append("current-system")
|
||||||
|
pathSwBin = pathCurrentSystem.Append("sw/bin/")
|
||||||
|
pathShell = pathSwBin.Append(bash)
|
||||||
|
|
||||||
|
pathData = container.MustAbs("/data")
|
||||||
|
pathDataData = pathData.Append("data")
|
||||||
|
)
|
||||||
|
|
||||||
|
func lookPath(file string) string {
|
||||||
|
if p, err := exec.LookPath(file); err != nil {
|
||||||
|
log.Fatalf("%s: command not found", file)
|
||||||
|
return ""
|
||||||
|
} else {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var beforeRunFail = new(atomic.Pointer[func()])
|
||||||
|
|
||||||
|
func mustRun(name string, arg ...string) {
|
||||||
|
hlog.Verbosef("spawning process: %q %q", name, arg)
|
||||||
|
cmd := exec.Command(name, arg...)
|
||||||
|
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
if f := beforeRunFail.Swap(nil); f != nil {
|
||||||
|
(*f)()
|
||||||
|
}
|
||||||
|
log.Fatalf("%s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type appPathSet struct {
|
||||||
|
// ${dataHome}/${id}
|
||||||
|
baseDir *container.Absolute
|
||||||
|
// ${baseDir}/app
|
||||||
|
metaPath *container.Absolute
|
||||||
|
// ${baseDir}/files
|
||||||
|
homeDir *container.Absolute
|
||||||
|
// ${baseDir}/cache
|
||||||
|
cacheDir *container.Absolute
|
||||||
|
// ${baseDir}/cache/nix
|
||||||
|
nixPath *container.Absolute
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathSetByApp(id string) *appPathSet {
|
||||||
|
pathSet := new(appPathSet)
|
||||||
|
pathSet.baseDir = dataHome.Append(id)
|
||||||
|
pathSet.metaPath = pathSet.baseDir.Append("app")
|
||||||
|
pathSet.homeDir = pathSet.baseDir.Append("files")
|
||||||
|
pathSet.cacheDir = pathSet.baseDir.Append("cache")
|
||||||
|
pathSet.nixPath = pathSet.cacheDir.Append("nix")
|
||||||
|
return pathSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendGPUFilesystem(config *hst.Config) {
|
||||||
|
config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfigJSON{
|
||||||
|
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("dri"), Device: true, Optional: true}},
|
||||||
|
// mali
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("mali"), Device: true, Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("mali0"), Device: true, Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("umplock"), Device: true, Optional: true}},
|
||||||
|
// nvidia
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidiactl"), Device: true, Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-modeset"), Device: true, Optional: true}},
|
||||||
|
// nvidia OpenCL/CUDA
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-uvm"), Device: true, Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-uvm-tools"), Device: true, Optional: true}},
|
||||||
|
|
||||||
|
// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia0"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia1"), Device: true, Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia2"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia3"), Device: true, Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia4"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia5"), Device: true, Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia6"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia7"), Device: true, Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia8"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia9"), Device: true, Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia10"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia11"), Device: true, Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia12"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia13"), Device: true, Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia14"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia15"), Device: true, Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia16"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia17"), Device: true, Optional: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia18"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia19"), Device: true, Optional: true}},
|
||||||
|
}...)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
users.users = {
|
||||||
|
alice = {
|
||||||
|
isNormalUser = true;
|
||||||
|
description = "Alice Foobar";
|
||||||
|
password = "foobar";
|
||||||
|
uid = 1000;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
home-manager.users.alice.home.stateVersion = "24.11";
|
||||||
|
|
||||||
|
# Automatically login on tty1 as a normal user:
|
||||||
|
services.getty.autologinUser = "alice";
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
variables = {
|
||||||
|
SWAYSOCK = "/tmp/sway-ipc.sock";
|
||||||
|
WLR_RENDERER = "pixman";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Automatically configure and start Sway when logging in on tty1:
|
||||||
|
programs.bash.loginShellInit = ''
|
||||||
|
if [ "$(tty)" = "/dev/tty1" ]; then
|
||||||
|
set -e
|
||||||
|
|
||||||
|
mkdir -p ~/.config/sway
|
||||||
|
(sed s/Mod4/Mod1/ /etc/sway/config &&
|
||||||
|
echo 'output * bg ${pkgs.nixos-artwork.wallpapers.simple-light-gray.gnomeFilePath} fill' &&
|
||||||
|
echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config
|
||||||
|
|
||||||
|
sway --validate
|
||||||
|
systemd-cat --identifier=session sway && touch /tmp/sway-exit-ok
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
programs.sway.enable = true;
|
||||||
|
|
||||||
|
virtualisation = {
|
||||||
|
diskSize = 6 * 1024;
|
||||||
|
|
||||||
|
qemu.options = [
|
||||||
|
# Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch:
|
||||||
|
"-vga none -device virtio-gpu-pci"
|
||||||
|
|
||||||
|
# Increase zstd performance:
|
||||||
|
"-smp 8"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.hakurei = {
|
||||||
|
enable = true;
|
||||||
|
stateDir = "/var/lib/hakurei";
|
||||||
|
users.alice = 0;
|
||||||
|
|
||||||
|
extraHomeConfig = {
|
||||||
|
home.stateVersion = "23.05";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
nixosTest,
|
||||||
|
callPackage,
|
||||||
|
|
||||||
|
system,
|
||||||
|
self,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
buildPackage = self.buildPackage.${system};
|
||||||
|
in
|
||||||
|
nixosTest {
|
||||||
|
name = "hpkg";
|
||||||
|
nodes.machine = {
|
||||||
|
environment.etc = {
|
||||||
|
"foot.pkg".source = callPackage ./foot.nix { inherit buildPackage; };
|
||||||
|
};
|
||||||
|
|
||||||
|
imports = [
|
||||||
|
./configuration.nix
|
||||||
|
|
||||||
|
self.nixosModules.hakurei
|
||||||
|
self.inputs.home-manager.nixosModules.home-manager
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# adapted from nixos sway integration tests
|
||||||
|
|
||||||
|
# testScriptWithTypes:49: error: Cannot call function of unknown type
|
||||||
|
# (machine.succeed if succeed else machine.execute)(
|
||||||
|
# ^
|
||||||
|
# Found 1 error in 1 file (checked 1 source file)
|
||||||
|
skipTypeCheck = true;
|
||||||
|
testScript = builtins.readFile ./test.py;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
buildPackage,
|
||||||
|
foot,
|
||||||
|
wayland-utils,
|
||||||
|
inconsolata,
|
||||||
|
}:
|
||||||
|
|
||||||
|
buildPackage {
|
||||||
|
name = "foot";
|
||||||
|
inherit (foot) version;
|
||||||
|
|
||||||
|
identity = 2;
|
||||||
|
id = "org.codeberg.dnkl.foot";
|
||||||
|
|
||||||
|
modules = [
|
||||||
|
{
|
||||||
|
home.packages = [
|
||||||
|
foot
|
||||||
|
|
||||||
|
# For wayland-info:
|
||||||
|
wayland-utils
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
nixosModules = [
|
||||||
|
{
|
||||||
|
# To help with OCR:
|
||||||
|
environment.etc."xdg/foot/foot.ini".text = lib.generators.toINI { } {
|
||||||
|
main = {
|
||||||
|
font = "inconsolata:size=14";
|
||||||
|
};
|
||||||
|
colors = rec {
|
||||||
|
foreground = "000000";
|
||||||
|
background = "ffffff";
|
||||||
|
regular2 = foreground;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
fonts.packages = [ inconsolata ];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
script = ''
|
||||||
|
exec foot "$@"
|
||||||
|
'';
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import json
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
q = shlex.quote
|
||||||
|
NODE_GROUPS = ["nodes", "floating_nodes"]
|
||||||
|
|
||||||
|
|
||||||
|
def swaymsg(command: str = "", succeed=True, type="command"):
|
||||||
|
assert command != "" or type != "command", "Must specify command or type"
|
||||||
|
shell = q(f"swaymsg -t {q(type)} -- {q(command)}")
|
||||||
|
with machine.nested(
|
||||||
|
f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)
|
||||||
|
):
|
||||||
|
ret = (machine.succeed if succeed else machine.execute)(
|
||||||
|
f"su - alice -c {shell}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# execute also returns a status code, but disregard.
|
||||||
|
if not succeed:
|
||||||
|
_, ret = ret
|
||||||
|
|
||||||
|
if not succeed and not ret:
|
||||||
|
return None
|
||||||
|
|
||||||
|
parsed = json.loads(ret)
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def walk(tree):
|
||||||
|
yield tree
|
||||||
|
for group in NODE_GROUPS:
|
||||||
|
for node in tree.get(group, []):
|
||||||
|
yield from walk(node)
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_window(pattern):
|
||||||
|
def func(last_chance):
|
||||||
|
nodes = (node["name"] for node in walk(swaymsg(type="get_tree")))
|
||||||
|
|
||||||
|
if last_chance:
|
||||||
|
nodes = list(nodes)
|
||||||
|
machine.log(f"Last call! Current list of windows: {nodes}")
|
||||||
|
|
||||||
|
return any(pattern in name for name in nodes)
|
||||||
|
|
||||||
|
retry(func)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_state_ui(name):
|
||||||
|
swaymsg(f"exec hakurei ps > '/tmp/{name}.ps'")
|
||||||
|
machine.copy_from_vm(f"/tmp/{name}.ps", "")
|
||||||
|
swaymsg(f"exec hakurei --json ps > '/tmp/{name}.json'")
|
||||||
|
machine.copy_from_vm(f"/tmp/{name}.json", "")
|
||||||
|
machine.screenshot(name)
|
||||||
|
|
||||||
|
|
||||||
|
def check_state(name, enablements):
|
||||||
|
instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei --json ps"))
|
||||||
|
if len(instances) != 1:
|
||||||
|
raise Exception(f"unexpected state length {len(instances)}")
|
||||||
|
instance = next(iter(instances.values()))
|
||||||
|
|
||||||
|
config = instance['config']
|
||||||
|
|
||||||
|
if len(config['args']) != 1 or not (config['args'][0].startswith("/nix/store/")) or f"hakurei-{name}-" not in (config['args'][0]):
|
||||||
|
raise Exception(f"unexpected args {instance['config']['args']}")
|
||||||
|
|
||||||
|
if config['enablements'] != enablements:
|
||||||
|
raise Exception(f"unexpected enablements {instance['config']['enablements']}")
|
||||||
|
|
||||||
|
|
||||||
|
start_all()
|
||||||
|
machine.wait_for_unit("multi-user.target")
|
||||||
|
|
||||||
|
# To check hakurei's version:
|
||||||
|
print(machine.succeed("sudo -u alice -i hakurei version"))
|
||||||
|
|
||||||
|
# Wait for Sway to complete startup:
|
||||||
|
machine.wait_for_file("/run/user/1000/wayland-1")
|
||||||
|
machine.wait_for_file("/tmp/sway-ipc.sock")
|
||||||
|
|
||||||
|
# Prepare hpkg directory:
|
||||||
|
machine.succeed("install -dm 0700 -o alice -g users /var/lib/hakurei/1000")
|
||||||
|
|
||||||
|
# Install hpkg app:
|
||||||
|
swaymsg("exec hpkg -v install /etc/foot.pkg && touch /tmp/hpkg-install-ok")
|
||||||
|
machine.wait_for_file("/tmp/hpkg-install-ok")
|
||||||
|
|
||||||
|
# Start app (foot) with Wayland enablement:
|
||||||
|
swaymsg("exec hpkg -v start org.codeberg.dnkl.foot")
|
||||||
|
wait_for_window("hakurei@machine-foot")
|
||||||
|
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
|
||||||
|
machine.wait_for_file("/tmp/hakurei.0/tmpdir/2/success-client")
|
||||||
|
collect_state_ui("app_wayland")
|
||||||
|
check_state("foot", {"wayland": True, "dbus": True, "pulse": True})
|
||||||
|
# Verify acl on XDG_RUNTIME_DIR:
|
||||||
|
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
|
||||||
|
machine.send_chars("exit\n")
|
||||||
|
machine.wait_until_fails("pgrep foot")
|
||||||
|
# Verify acl cleanup on XDG_RUNTIME_DIR:
|
||||||
|
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002")
|
||||||
|
|
||||||
|
# Exit Sway and verify process exit status 0:
|
||||||
|
swaymsg("exit", succeed=False)
|
||||||
|
machine.wait_for_file("/tmp/sway-exit-ok")
|
||||||
|
|
||||||
|
# Print hakurei runDir contents:
|
||||||
|
print(machine.succeed("find /run/user/1000/hakurei"))
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"hakurei.app/container"
|
||||||
|
"hakurei.app/container/seccomp"
|
||||||
|
"hakurei.app/hst"
|
||||||
|
"hakurei.app/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func withNixDaemon(
|
||||||
|
ctx context.Context,
|
||||||
|
action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config,
|
||||||
|
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
|
||||||
|
) {
|
||||||
|
mustRunAppDropShell(ctx, updateConfig(&hst.Config{
|
||||||
|
ID: app.ID,
|
||||||
|
|
||||||
|
Path: pathShell,
|
||||||
|
Args: []string{bash, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
|
||||||
|
// start nix-daemon
|
||||||
|
"nix-daemon --store / & " +
|
||||||
|
// wait for socket to appear
|
||||||
|
"(while [ ! -S /nix/var/nix/daemon-socket/socket ]; do sleep 0.01; done) && " +
|
||||||
|
// create directory so nix stops complaining
|
||||||
|
"mkdir -p /nix/var/nix/profiles/per-user/root/channels && " +
|
||||||
|
strings.Join(command, " && ") +
|
||||||
|
// terminate nix-daemon
|
||||||
|
" && pkill nix-daemon",
|
||||||
|
},
|
||||||
|
|
||||||
|
Username: "hakurei",
|
||||||
|
Shell: pathShell,
|
||||||
|
Home: pathDataData.Append(app.ID),
|
||||||
|
ExtraPerms: []*hst.ExtraPermConfig{
|
||||||
|
{Path: dataHome, Execute: true},
|
||||||
|
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
|
||||||
|
},
|
||||||
|
|
||||||
|
Identity: app.Identity,
|
||||||
|
|
||||||
|
Container: &hst.ContainerConfig{
|
||||||
|
Hostname: formatHostname(app.Name) + "-" + action,
|
||||||
|
Userns: true, // nix sandbox requires userns
|
||||||
|
HostNet: net,
|
||||||
|
SeccompFlags: seccomp.AllowMultiarch,
|
||||||
|
Tty: dropShell,
|
||||||
|
Filesystem: []hst.FilesystemConfigJSON{
|
||||||
|
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath, Target: pathNix, Write: true}},
|
||||||
|
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
|
||||||
|
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
|
||||||
|
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}), dropShell, beforeFail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func withCacheDir(
|
||||||
|
ctx context.Context,
|
||||||
|
action string, command []string, workDir *container.Absolute,
|
||||||
|
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
|
||||||
|
mustRunAppDropShell(ctx, &hst.Config{
|
||||||
|
ID: app.ID,
|
||||||
|
|
||||||
|
Path: pathShell,
|
||||||
|
Args: []string{bash, "-lc", strings.Join(command, " && ")},
|
||||||
|
|
||||||
|
Username: "nixos",
|
||||||
|
Shell: pathShell,
|
||||||
|
Home: pathDataData.Append(app.ID, "cache"),
|
||||||
|
ExtraPerms: []*hst.ExtraPermConfig{
|
||||||
|
{Path: dataHome, Execute: true},
|
||||||
|
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
|
||||||
|
{Path: workDir, Execute: true},
|
||||||
|
},
|
||||||
|
|
||||||
|
Identity: app.Identity,
|
||||||
|
|
||||||
|
Container: &hst.ContainerConfig{
|
||||||
|
Hostname: formatHostname(app.Name) + "-" + action,
|
||||||
|
SeccompFlags: seccomp.AllowMultiarch,
|
||||||
|
Tty: dropShell,
|
||||||
|
Filesystem: []hst.FilesystemConfigJSON{
|
||||||
|
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: workDir.Append(container.FHSEtc), Special: true}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: workDir.Append("nix"), Target: pathNix}},
|
||||||
|
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
|
||||||
|
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
|
||||||
|
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Source: workDir, Target: hst.AbsTmp.Append("bundle")}},
|
||||||
|
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID, "cache"), Source: pathSet.cacheDir, Write: true, Ensure: true}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, dropShell, beforeFail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustRunAppDropShell(ctx context.Context, config *hst.Config, dropShell bool, beforeFail func()) {
|
||||||
|
if dropShell {
|
||||||
|
config.Args = []string{bash, "-l"}
|
||||||
|
mustRunApp(ctx, config, beforeFail)
|
||||||
|
beforeFail()
|
||||||
|
internal.Exit(0)
|
||||||
|
}
|
||||||
|
mustRunApp(ctx, config, beforeFail)
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
//go:build !rosa
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
// hsuConfPath is an absolute pathname to the hsu configuration file. Its
|
|
||||||
// contents are interpreted by parseConfig.
|
|
||||||
const hsuConfPath = "/etc/hsurc"
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
//go:build rosa
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
// hsuConfPath is the pathname to the hsu configuration file, specific to
|
|
||||||
// Rosa OS. Its contents are interpreted by parseConfig.
|
|
||||||
const hsuConfPath = "/system/etc/hsurc"
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
/* keep in sync with hst */
|
|
||||||
|
|
||||||
const (
|
|
||||||
userOffset = 100000
|
|
||||||
rangeSize = userOffset / 10
|
|
||||||
|
|
||||||
identityStart = 0
|
|
||||||
identityEnd = appEnd - appStart
|
|
||||||
|
|
||||||
appStart = rangeSize * 1
|
|
||||||
appEnd = appStart + rangeSize - 1
|
|
||||||
)
|
|
||||||
|
|
||||||
func toUser(userid, appid uint32) uint32 { return userid*userOffset + appStart + appid }
|
|
||||||
+43
-115
@@ -1,56 +1,3 @@
|
|||||||
// hsu starts the hakurei shim as the target subordinate user.
|
|
||||||
//
|
|
||||||
// The hsu program must be installed with the setuid and setgid bit set, and
|
|
||||||
// owned by root. A configuration file must be installed at /etc/hsurc with
|
|
||||||
// permission bits 0400, and owned by root. Each line of the file specifies a
|
|
||||||
// hakurei userid to kernel uid mapping. A line consists of the decimal string
|
|
||||||
// representation of the uid of the user wishing to start hakurei containers,
|
|
||||||
// followed by a space, followed by the decimal string representation of its
|
|
||||||
// userid. Duplicate uid entries are ignored, with the first occurrence taking
|
|
||||||
// effect.
|
|
||||||
//
|
|
||||||
// For example, to map the kernel uid 1000 to the hakurei user id 0:
|
|
||||||
//
|
|
||||||
// 1000 0
|
|
||||||
//
|
|
||||||
// # Internals
|
|
||||||
//
|
|
||||||
// Hakurei and hsu holds pathnames pointing to each other set at link time. For
|
|
||||||
// this reason, a distribution of hakurei has fixed installation prefix. Since
|
|
||||||
// this program is never invoked by the user, behaviour described in the
|
|
||||||
// following paragraphs are considered an internal detail and not covered by the
|
|
||||||
// compatibility promise.
|
|
||||||
//
|
|
||||||
// After checking credentials, hsu checks via /proc/ the absolute pathname of
|
|
||||||
// its parent process, and fails if it does not match the hakurei pathname set
|
|
||||||
// at link time. This is not a security feature: the priv-side is considered
|
|
||||||
// trusted, and this feature makes no attempt to address the racy nature of
|
|
||||||
// querying /proc/, or debuggers attached to the parent process. Instead, this
|
|
||||||
// aims to discourage misuse and reduce confusion if the user accidentally
|
|
||||||
// stumbles upon this program. It also prevents accidental use of the incorrect
|
|
||||||
// installation of hsu in some environments.
|
|
||||||
//
|
|
||||||
// Since target container environment variables are set up in shim via the
|
|
||||||
// [container] infrastructure, the environment is used for parameters from the
|
|
||||||
// parent process.
|
|
||||||
//
|
|
||||||
// HAKUREI_SHIM specifies a single byte between '3' and '9' representing the
|
|
||||||
// setup pipe file descriptor. It is passed as is to the shim process and is the
|
|
||||||
// only value in the environment of the shim process. Since hsurc is not
|
|
||||||
// accessible to the parent process, leaving this unset causes hsu to print the
|
|
||||||
// corresponding hakurei user id of the parent and terminate.
|
|
||||||
//
|
|
||||||
// HAKUREI_IDENTITY specifies the identity of the instance being started and is
|
|
||||||
// used to produce the kernel uid alongside hakurei user id looked up from hsurc.
|
|
||||||
//
|
|
||||||
// HAKUREI_GROUPS specifies supplementary groups to inherit from the credentials
|
|
||||||
// of the parent process in a ' ' separated list of decimal string
|
|
||||||
// representations of gid. This has the unfortunate consequence of allowing
|
|
||||||
// users mapped via hsurc to effectively drop group membership, so special care
|
|
||||||
// must be taken to ensure this does not lead to an increase in access. This is
|
|
||||||
// not applicable to Rosa OS since unsigned code execution is not permitted
|
|
||||||
// outside hakurei containers, and is generally nonapplicable to the security
|
|
||||||
// model of hakurei, where all untrusted code runs within containers.
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -58,8 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path"
|
||||||
"runtime"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -67,60 +13,47 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// envShim is the name of the environment variable holding a single byte
|
hsuConfFile = "/etc/hsurc"
|
||||||
// representing the shim setup pipe file descriptor.
|
envShim = "HAKUREI_SHIM"
|
||||||
envShim = "HAKUREI_SHIM"
|
envAID = "HAKUREI_APP_ID"
|
||||||
// envIdentity is the name of the environment variable holding a decimal
|
envGroups = "HAKUREI_GROUPS"
|
||||||
// string representation of the current application identity.
|
|
||||||
envIdentity = "HAKUREI_IDENTITY"
|
PR_SET_NO_NEW_PRIVS = 0x26
|
||||||
// envGroups holds a ' ' separated list of decimal string representations of
|
|
||||||
// supplementary group gid. Membership requirements are enforced.
|
|
||||||
envGroups = "HAKUREI_GROUPS"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// hakureiPath is the absolute path to Hakurei.
|
|
||||||
//
|
|
||||||
// This is set by the linker.
|
|
||||||
var hakureiPath string
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
const PR_SET_NO_NEW_PRIVS = 0x26
|
|
||||||
runtime.LockOSThread()
|
|
||||||
|
|
||||||
log.SetFlags(0)
|
log.SetFlags(0)
|
||||||
log.SetPrefix("hsu: ")
|
log.SetPrefix("hsu: ")
|
||||||
|
log.SetOutput(os.Stderr)
|
||||||
|
|
||||||
if os.Geteuid() != 0 {
|
if os.Geteuid() != 0 {
|
||||||
log.Fatal("this program must be owned by uid 0 and have the setuid bit set")
|
log.Fatal("this program must be owned by uid 0 and have the setuid bit set")
|
||||||
}
|
}
|
||||||
if os.Getegid() != os.Getgid() {
|
|
||||||
log.Fatal("this program must not have the setgid bit set")
|
|
||||||
}
|
|
||||||
|
|
||||||
puid := os.Getuid()
|
puid := os.Getuid()
|
||||||
if puid == 0 {
|
if puid == 0 {
|
||||||
log.Fatal("this program must not be started by root")
|
log.Fatal("this program must not be started by root")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !filepath.IsAbs(hakureiPath) {
|
|
||||||
log.Fatal("this program is compiled incorrectly")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var toolPath string
|
var toolPath string
|
||||||
pexe := filepath.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
|
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
|
||||||
if p, err := os.Readlink(pexe); err != nil {
|
if p, err := os.Readlink(pexe); err != nil {
|
||||||
log.Fatalf("cannot read parent executable path: %v", err)
|
log.Fatalf("cannot read parent executable path: %v", err)
|
||||||
} else if strings.HasSuffix(p, " (deleted)") {
|
} else if strings.HasSuffix(p, " (deleted)") {
|
||||||
log.Fatal("hakurei executable has been deleted")
|
log.Fatal("hakurei executable has been deleted")
|
||||||
} else if p != hakureiPath {
|
} else if p != mustCheckPath(hmain) {
|
||||||
log.Fatal("this program must be started by hakurei")
|
log.Fatal("this program must be started by hakurei")
|
||||||
} else {
|
} else {
|
||||||
toolPath = p
|
toolPath = p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// uid = 1000000 +
|
||||||
|
// fid * 10000 +
|
||||||
|
// aid
|
||||||
|
uid := 1000000
|
||||||
|
|
||||||
// refuse to run if hsurc is not protected correctly
|
// refuse to run if hsurc is not protected correctly
|
||||||
if s, err := os.Stat(hsuConfPath); err != nil {
|
if s, err := os.Stat(hsuConfFile); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
} else if s.Mode().Perm() != 0400 {
|
} else if s.Mode().Perm() != 0400 {
|
||||||
log.Fatal("bad hsurc perm")
|
log.Fatal("bad hsurc perm")
|
||||||
@@ -129,13 +62,29 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// authenticate before accepting user input
|
// authenticate before accepting user input
|
||||||
userid := mustParseConfig(puid)
|
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 hsurc file", puid)
|
||||||
|
} else {
|
||||||
|
uid += fid * 10000
|
||||||
|
}
|
||||||
|
|
||||||
|
// allowed aid range 0 to 9999
|
||||||
|
if as, ok := os.LookupEnv(envAID); !ok {
|
||||||
|
log.Fatal("HAKUREI_APP_ID not set")
|
||||||
|
} else if aid, err := parseUint32Fast(as); err != nil || aid < 0 || aid > 9999 {
|
||||||
|
log.Fatal("invalid aid")
|
||||||
|
} else {
|
||||||
|
uid += aid
|
||||||
|
}
|
||||||
|
|
||||||
// pass through setup fd to shim
|
// pass through setup fd to shim
|
||||||
var shimSetupFd string
|
var shimSetupFd string
|
||||||
if s, ok := os.LookupEnv(envShim); !ok {
|
if s, ok := os.LookupEnv(envShim); !ok {
|
||||||
// hakurei requests hsurc user id
|
// hakurei requests target uid
|
||||||
fmt.Print(userid)
|
// print resolved uid and exit
|
||||||
|
fmt.Print(uid)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' {
|
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' {
|
||||||
log.Fatal("HAKUREI_SHIM holds an invalid value")
|
log.Fatal("HAKUREI_SHIM holds an invalid value")
|
||||||
@@ -143,22 +92,6 @@ func main() {
|
|||||||
shimSetupFd = s
|
shimSetupFd = s
|
||||||
}
|
}
|
||||||
|
|
||||||
// start is going ahead at this point
|
|
||||||
identity := mustReadIdentity()
|
|
||||||
|
|
||||||
const (
|
|
||||||
// first possible uid outcome
|
|
||||||
uidStart = 10000
|
|
||||||
// last possible uid outcome
|
|
||||||
uidEnd = 999919999
|
|
||||||
)
|
|
||||||
uid := int(toUser(userid, identity))
|
|
||||||
|
|
||||||
// final bounds check to catch any bugs
|
|
||||||
if uid < uidStart || uid >= uidEnd {
|
|
||||||
panic("uid out of bounds")
|
|
||||||
}
|
|
||||||
|
|
||||||
// supplementary groups
|
// supplementary groups
|
||||||
var suppGroups, suppCurrent []int
|
var suppGroups, suppCurrent []int
|
||||||
|
|
||||||
@@ -186,7 +119,13 @@ func main() {
|
|||||||
suppGroups = []int{uid}
|
suppGroups = []int{uid}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// final bounds check to catch any bugs
|
||||||
|
if uid < 1000000 || uid >= 2000000 {
|
||||||
|
panic("uid out of bounds")
|
||||||
|
}
|
||||||
|
|
||||||
// careful! users in the allowlist is effectively allowed to drop groups via hsu
|
// careful! users in the allowlist is effectively allowed to drop groups via hsu
|
||||||
|
|
||||||
if err := syscall.Setresgid(uid, uid, uid); err != nil {
|
if err := syscall.Setresgid(uid, uid, uid); err != nil {
|
||||||
log.Fatalf("cannot set gid: %v", err)
|
log.Fatalf("cannot set gid: %v", err)
|
||||||
}
|
}
|
||||||
@@ -196,21 +135,10 @@ func main() {
|
|||||||
if err := syscall.Setresuid(uid, uid, uid); err != nil {
|
if err := syscall.Setresuid(uid, uid, uid); err != nil {
|
||||||
log.Fatalf("cannot set uid: %v", err)
|
log.Fatalf("cannot set uid: %v", err)
|
||||||
}
|
}
|
||||||
|
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
|
||||||
if _, _, errno := syscall.AllThreadsSyscall(
|
|
||||||
syscall.SYS_PRCTL,
|
|
||||||
PR_SET_NO_NEW_PRIVS, 1,
|
|
||||||
0,
|
|
||||||
); errno != 0 {
|
|
||||||
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
|
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
|
||||||
}
|
}
|
||||||
|
if err := syscall.Exec(toolPath, []string{"hakurei", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
|
||||||
if err := syscall.Exec(toolPath, []string{
|
|
||||||
"hakurei",
|
|
||||||
"shim",
|
|
||||||
}, []string{
|
|
||||||
envShim + "=" + shimSetupFd,
|
|
||||||
}); err != nil {
|
|
||||||
log.Fatalf("cannot start shim: %v", err)
|
log.Fatalf("cannot start shim: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -19,5 +19,5 @@ buildGoModule {
|
|||||||
ldflags = lib.attrsets.foldlAttrs (
|
ldflags = lib.attrsets.foldlAttrs (
|
||||||
ldflags: name: value:
|
ldflags: name: value:
|
||||||
ldflags ++ [ "-X main.${name}=${value}" ]
|
ldflags ++ [ "-X main.${name}=${value}" ]
|
||||||
) [ "-s -w" ] { hakureiPath = "${hakurei}/libexec/hakurei"; };
|
) [ "-s -w" ] { hmain = "${hakurei}/libexec/hakurei"; };
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-80
@@ -6,123 +6,62 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
func parseUint32Fast(s string) (int, error) {
|
||||||
// useridStart is the first userid.
|
|
||||||
useridStart = 0
|
|
||||||
// useridEnd is the last userid.
|
|
||||||
useridEnd = useridStart + rangeSize - 1
|
|
||||||
)
|
|
||||||
|
|
||||||
// parseUint32Fast parses a string representation of an unsigned 32-bit integer
|
|
||||||
// value using the fast path only. This limits the range of values it is defined
|
|
||||||
// in but is perfectly adequate for this use case.
|
|
||||||
func parseUint32Fast(s string) (uint32, error) {
|
|
||||||
sLen := len(s)
|
sLen := len(s)
|
||||||
if sLen < 1 {
|
if sLen < 1 {
|
||||||
return 0, errors.New("zero length string")
|
return -1, errors.New("zero length string")
|
||||||
}
|
}
|
||||||
if sLen > 10 {
|
if sLen > 10 {
|
||||||
return 0, errors.New("string too long")
|
return -1, errors.New("string too long")
|
||||||
}
|
}
|
||||||
|
|
||||||
var n uint32
|
n := 0
|
||||||
for i, ch := range []byte(s) {
|
for i, ch := range []byte(s) {
|
||||||
ch -= '0'
|
ch -= '0'
|
||||||
if ch > 9 {
|
if ch > 9 {
|
||||||
return 0, fmt.Errorf("invalid character '%s' at index %d", string(ch+'0'), i)
|
return -1, fmt.Errorf("invalid character '%s' at index %d", string(ch+'0'), i)
|
||||||
}
|
}
|
||||||
n = n*10 + uint32(ch)
|
n = n*10 + int(ch)
|
||||||
}
|
}
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseConfig reads a list of allowed users from r until it encounters puid or
|
func parseConfig(r io.Reader, puid int) (fid int, ok bool, err error) {
|
||||||
// [io.EOF].
|
|
||||||
//
|
|
||||||
// Each line of the file specifies a hakurei userid to kernel uid mapping. A
|
|
||||||
// line consists of the string representation of the uid of the user wishing to
|
|
||||||
// start hakurei containers, followed by a space, followed by the string
|
|
||||||
// representation of its userid. Duplicate uid entries are ignored, with the
|
|
||||||
// first occurrence taking effect.
|
|
||||||
//
|
|
||||||
// All string representations are parsed by calling parseUint32Fast.
|
|
||||||
func parseConfig(r io.Reader, puid uint32) (userid uint32, ok bool, err error) {
|
|
||||||
s := bufio.NewScanner(r)
|
s := bufio.NewScanner(r)
|
||||||
var (
|
var line, puid0 int
|
||||||
line uintptr
|
|
||||||
puid0 uint32
|
|
||||||
)
|
|
||||||
for s.Scan() {
|
for s.Scan() {
|
||||||
line++
|
line++
|
||||||
|
|
||||||
// <puid> <userid>
|
// <puid> <fid>
|
||||||
lf := strings.SplitN(s.Text(), " ", 2)
|
lf := strings.SplitN(s.Text(), " ", 2)
|
||||||
if len(lf) != 2 {
|
if len(lf) != 2 {
|
||||||
return useridEnd + 1, false, fmt.Errorf("invalid entry on line %d", line)
|
return -1, false, fmt.Errorf("invalid entry on line %d", line)
|
||||||
}
|
}
|
||||||
|
|
||||||
puid0, err = parseUint32Fast(lf[0])
|
puid0, err = parseUint32Fast(lf[0])
|
||||||
if err != nil || puid0 < 1 {
|
if err != nil || puid0 < 1 {
|
||||||
return useridEnd + 1, false, fmt.Errorf("invalid parent uid on line %d", line)
|
return -1, false, fmt.Errorf("invalid parent uid on line %d", line)
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = puid0 == puid
|
ok = puid0 == puid
|
||||||
if ok {
|
if ok {
|
||||||
// userid bound to a range, uint32 size allows this to be increased if needed
|
// allowed fid range 0 to 99
|
||||||
if userid, err = parseUint32Fast(lf[1]); err != nil ||
|
if fid, err = parseUint32Fast(lf[1]); err != nil || fid < 0 || fid > 99 {
|
||||||
userid < useridStart || userid > useridEnd {
|
return -1, false, fmt.Errorf("invalid identity on line %d", line)
|
||||||
return useridEnd + 1, false, fmt.Errorf("invalid userid on line %d", line)
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return useridEnd + 1, false, s.Err()
|
return -1, false, s.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// mustParseConfig calls parseConfig to interpret the contents of hsuConfPath,
|
func mustParseConfig(r io.Reader, puid int) (int, bool) {
|
||||||
// terminating the program if an error is encountered, the syntax is incorrect,
|
fid, ok, err := parseConfig(r, puid)
|
||||||
// or the current user is not authorised to use hsu because its uid is missing.
|
if err != nil {
|
||||||
//
|
|
||||||
// Therefore, code after this function call can assume an authenticated state.
|
|
||||||
//
|
|
||||||
// mustParseConfig returns the userid value of the current user.
|
|
||||||
func mustParseConfig(puid int) (userid uint32) {
|
|
||||||
if puid > math.MaxUint32 {
|
|
||||||
log.Fatalf("got impossible uid %d", puid)
|
|
||||||
}
|
|
||||||
|
|
||||||
var ok bool
|
|
||||||
if f, err := os.Open(hsuConfPath); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
} else if userid, ok, err = parseConfig(f, uint32(puid)); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
} else if err = f.Close(); err != nil {
|
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
if !ok {
|
return fid, ok
|
||||||
log.Fatalf("uid %d is not in the hsurc file", puid)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// mustReadIdentity calls parseUint32Fast to interpret the value stored in envIdentity,
|
|
||||||
// terminating the program if the value is not set, malformed, or out of bounds.
|
|
||||||
func mustReadIdentity() uint32 {
|
|
||||||
// ranges defined in hst and copied to this package to avoid importing hst
|
|
||||||
if as, ok := os.LookupEnv(envIdentity); !ok {
|
|
||||||
log.Fatal("HAKUREI_IDENTITY not set")
|
|
||||||
panic("unreachable")
|
|
||||||
} else if identity, err := parseUint32Fast(as); err != nil ||
|
|
||||||
identity < identityStart || identity > identityEnd {
|
|
||||||
log.Fatal("invalid identity")
|
|
||||||
panic("unreachable")
|
|
||||||
} else {
|
|
||||||
return identity
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-41
@@ -2,105 +2,94 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"math"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseUint32Fast(t *testing.T) {
|
func Test_parseUint32Fast(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("zero-length", func(t *testing.T) {
|
t.Run("zero-length", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
if _, err := parseUint32Fast(""); err == nil || err.Error() != "zero length string" {
|
if _, err := parseUint32Fast(""); err == nil || err.Error() != "zero length string" {
|
||||||
t.Errorf(`parseUint32Fast(""): error = %v`, err)
|
t.Errorf(`parseUint32Fast(""): error = %v`, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("overflow", func(t *testing.T) {
|
t.Run("overflow", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
if _, err := parseUint32Fast("10000000000"); err == nil || err.Error() != "string too long" {
|
if _, err := parseUint32Fast("10000000000"); err == nil || err.Error() != "string too long" {
|
||||||
t.Errorf("parseUint32Fast: error = %v", err)
|
t.Errorf("parseUint32Fast: error = %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("invalid byte", func(t *testing.T) {
|
t.Run("invalid byte", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
if _, err := parseUint32Fast("meow"); err == nil || err.Error() != "invalid character 'm' at index 0" {
|
if _, err := parseUint32Fast("meow"); err == nil || err.Error() != "invalid character 'm' at index 0" {
|
||||||
t.Errorf(`parseUint32Fast("meow"): error = %v`, err)
|
t.Errorf(`parseUint32Fast("meow"): error = %v`, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
t.Run("full range", func(t *testing.T) {
|
||||||
t.Run("range", func(t *testing.T) {
|
testRange := func(i, end int) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testRange := func(i, end uint32) {
|
|
||||||
for ; i < end; i++ {
|
for ; i < end; i++ {
|
||||||
s := strconv.Itoa(int(i))
|
s := strconv.Itoa(i)
|
||||||
w := i
|
w := i
|
||||||
t.Run("parse "+s, func(t *testing.T) {
|
t.Run("parse "+s, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
v, err := parseUint32Fast(s)
|
v, err := parseUint32Fast(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("parseUint32Fast(%q): error = %v", s, err)
|
t.Errorf("parseUint32Fast(%q): error = %v",
|
||||||
|
s, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if v != w {
|
if v != w {
|
||||||
t.Errorf("parseUint32Fast(%q): got %v", s, v)
|
t.Errorf("parseUint32Fast(%q): got %v",
|
||||||
|
s, v)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
testRange(0, 2500)
|
testRange(0, 5000)
|
||||||
testRange(23002500, 23005000)
|
testRange(105000, 110000)
|
||||||
testRange(math.MaxUint32-2500, math.MaxUint32)
|
testRange(23005000, 23010000)
|
||||||
|
testRange(456005000, 456010000)
|
||||||
|
testRange(7890005000, 7890010000)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseConfig(t *testing.T) {
|
func Test_parseConfig(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
puid, want uint32
|
puid, want int
|
||||||
wantErr string
|
wantErr string
|
||||||
rc string
|
rc string
|
||||||
}{
|
}{
|
||||||
{"empty", 0, useridEnd + 1, "", ``},
|
{"empty", 0, -1, "", ``},
|
||||||
{"invalid field", 0, useridEnd + 1, "invalid entry on line 1", `9`},
|
{"invalid field", 0, -1, "invalid entry on line 1", `9`},
|
||||||
{"invalid puid", 0, useridEnd + 1, "invalid parent uid on line 1", `f 9`},
|
{"invalid puid", 0, -1, "invalid parent uid on line 1", `f 9`},
|
||||||
{"invalid userid", 1000, useridEnd + 1, "invalid userid on line 1", `1000 f`},
|
{"invalid fid", 1000, -1, "invalid identity on line 1", `1000 f`},
|
||||||
{"match", 1000, 0, "", `1000 0`},
|
{"match", 1000, 0, "", `1000 0`},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
fid, ok, err := parseConfig(bytes.NewBufferString(tc.rc), tc.puid)
|
||||||
|
|
||||||
userid, ok, err := parseConfig(bytes.NewBufferString(tc.rc), tc.puid)
|
|
||||||
if err == nil && tc.wantErr != "" {
|
if err == nil && tc.wantErr != "" {
|
||||||
t.Errorf("parseConfig: error = %v; want %q", err, tc.wantErr)
|
t.Errorf("parseConfig: error = %v; wantErr %q",
|
||||||
|
err, tc.wantErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil && err.Error() != tc.wantErr {
|
if err != nil && err.Error() != tc.wantErr {
|
||||||
t.Errorf("parseConfig: error = %q; want %q", err, tc.wantErr)
|
t.Errorf("parseConfig: error = %q; wantErr %q",
|
||||||
|
err, tc.wantErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ok == (tc.want == useridEnd+1) {
|
if ok == (tc.want == -1) {
|
||||||
t.Errorf("parseConfig: ok = %v; want %v", ok, tc.want)
|
t.Errorf("parseConfig: ok = %v; want %v",
|
||||||
|
ok, tc.want)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if userid != tc.want {
|
if fid != tc.want {
|
||||||
t.Errorf("parseConfig: %v; want %v", userid, tc.want)
|
t.Errorf("parseConfig: fid = %v; want %v",
|
||||||
|
fid, tc.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
||||||
|
|
||||||
|
var (
|
||||||
|
hmain = compPoison
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustCheckPath(p string) string {
|
||||||
|
if p != compPoison && p != "" && path.IsAbs(p) {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
log.Fatal("this program is compiled incorrectly")
|
||||||
|
return compPoison
|
||||||
|
}
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
"hakurei.app/container"
|
|
||||||
"hakurei.app/internal/pkg"
|
|
||||||
"hakurei.app/message"
|
|
||||||
)
|
|
||||||
|
|
||||||
// cache refers to an instance of [pkg.Cache] that might be open.
|
|
||||||
type cache struct {
|
|
||||||
ctx context.Context
|
|
||||||
msg message.Msg
|
|
||||||
|
|
||||||
// Should generally not be used directly.
|
|
||||||
c *pkg.Cache
|
|
||||||
|
|
||||||
cures, jobs int
|
|
||||||
// Primarily to work around missing landlock LSM.
|
|
||||||
hostAbstract bool
|
|
||||||
// Set SCHED_IDLE.
|
|
||||||
idle bool
|
|
||||||
// Unset [pkg.CSuppressInit].
|
|
||||||
verboseInit bool
|
|
||||||
// Loaded artifact of [rosa.QEMU].
|
|
||||||
qemu pkg.Artifact
|
|
||||||
|
|
||||||
base string
|
|
||||||
}
|
|
||||||
|
|
||||||
// open opens the underlying [pkg.Cache].
|
|
||||||
func (cache *cache) open() (err error) {
|
|
||||||
if cache.c != nil {
|
|
||||||
return os.ErrInvalid
|
|
||||||
}
|
|
||||||
|
|
||||||
var base *check.Absolute
|
|
||||||
if cache.base, err = filepath.Abs(cache.base); err != nil {
|
|
||||||
return
|
|
||||||
} else if base, err = check.NewAbs(cache.base); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var flags int
|
|
||||||
if cache.idle {
|
|
||||||
flags |= pkg.CSchedIdle
|
|
||||||
}
|
|
||||||
if cache.hostAbstract {
|
|
||||||
flags |= pkg.CHostAbstract
|
|
||||||
}
|
|
||||||
if !cache.verboseInit {
|
|
||||||
flags |= pkg.CSuppressInit
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
defer close(done)
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
case <-cache.ctx.Done():
|
|
||||||
if testing.Testing() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
os.Exit(2)
|
|
||||||
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
cache.msg.Verbosef("opening cache at %s", base)
|
|
||||||
cache.c, err = pkg.Open(
|
|
||||||
cache.ctx,
|
|
||||||
cache.msg,
|
|
||||||
flags,
|
|
||||||
cache.cures,
|
|
||||||
cache.jobs,
|
|
||||||
base,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
done <- struct{}{}
|
|
||||||
|
|
||||||
if cache.qemu != nil {
|
|
||||||
var pathname *check.Absolute
|
|
||||||
pathname, _, err = cache.c.Cure(cache.qemu)
|
|
||||||
if err != nil {
|
|
||||||
cache.c.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pkg.RegisterArch("riscv64", container.BinfmtEntry{
|
|
||||||
Offset: 0,
|
|
||||||
Magic: "\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xf3\x00",
|
|
||||||
Mask: "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff",
|
|
||||||
Interpreter: pathname.Append(
|
|
||||||
"system/bin",
|
|
||||||
"qemu-riscv64",
|
|
||||||
),
|
|
||||||
})
|
|
||||||
pkg.RegisterArch("arm64", container.BinfmtEntry{
|
|
||||||
Offset: 0,
|
|
||||||
Magic: "\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xb7\x00",
|
|
||||||
Mask: "\xff\xff\xff\xff\xff\xff\xff\xfc\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff",
|
|
||||||
Interpreter: pathname.Append(
|
|
||||||
"system/bin",
|
|
||||||
"qemu-aarch64",
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the underlying [pkg.Cache] if it is open.
|
|
||||||
func (cache *cache) Close() {
|
|
||||||
if cache.c != nil {
|
|
||||||
cache.c.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do calls f on the underlying cache and returns its error value.
|
|
||||||
func (cache *cache) Do(f func(cache *pkg.Cache) error) error {
|
|
||||||
if cache.c == nil {
|
|
||||||
if err := cache.open(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return f(cache.c)
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"hakurei.app/internal/pkg"
|
|
||||||
"hakurei.app/message"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCache(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cm := cache{
|
|
||||||
ctx: t.Context(),
|
|
||||||
msg: message.New(log.New(os.Stderr, "check: ", 0)),
|
|
||||||
base: t.TempDir(),
|
|
||||||
|
|
||||||
hostAbstract: true, idle: true,
|
|
||||||
}
|
|
||||||
defer cm.Close()
|
|
||||||
cm.Close()
|
|
||||||
|
|
||||||
if err := cm.open(); err != nil {
|
|
||||||
t.Fatalf("open: error = %v", err)
|
|
||||||
}
|
|
||||||
if err := cm.open(); err != os.ErrInvalid {
|
|
||||||
t.Errorf("(duplicate) open: error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cm.Do(func(cache *pkg.Cache) error {
|
|
||||||
return cache.Scrub(0)
|
|
||||||
}); err != nil {
|
|
||||||
t.Errorf("Scrub: error = %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,354 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"math"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
"unique"
|
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
"hakurei.app/internal/pkg"
|
|
||||||
)
|
|
||||||
|
|
||||||
// daemonTimeout is the maximum amount of time cureFromIR will wait on I/O.
|
|
||||||
const daemonTimeout = 30 * time.Second
|
|
||||||
|
|
||||||
// daemonDeadline returns the deadline corresponding to daemonTimeout, or the
|
|
||||||
// zero value when running in a test.
|
|
||||||
func daemonDeadline() time.Time {
|
|
||||||
if testing.Testing() {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
return time.Now().Add(daemonTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// remoteNoReply notifies that the client will not receive a cure reply.
|
|
||||||
remoteNoReply = 1 << iota
|
|
||||||
)
|
|
||||||
|
|
||||||
// cureFromIR services an IR curing request.
|
|
||||||
func cureFromIR(
|
|
||||||
cache *pkg.Cache,
|
|
||||||
conn net.Conn,
|
|
||||||
flags uint64,
|
|
||||||
) (pkg.Artifact, error) {
|
|
||||||
a, decodeErr := cache.NewDecoder(conn).Decode()
|
|
||||||
if decodeErr != nil {
|
|
||||||
_, err := conn.Write([]byte("\x00" + decodeErr.Error()))
|
|
||||||
return nil, errors.Join(decodeErr, err, conn.Close())
|
|
||||||
}
|
|
||||||
|
|
||||||
pathname, _, cureErr := cache.Cure(a)
|
|
||||||
if flags&remoteNoReply != 0 {
|
|
||||||
return a, errors.Join(cureErr, conn.Close())
|
|
||||||
}
|
|
||||||
if err := conn.SetWriteDeadline(daemonDeadline()); err != nil {
|
|
||||||
return a, errors.Join(cureErr, err, conn.Close())
|
|
||||||
}
|
|
||||||
if cureErr != nil {
|
|
||||||
_, err := conn.Write([]byte("\x00" + cureErr.Error()))
|
|
||||||
return a, errors.Join(cureErr, err, conn.Close())
|
|
||||||
}
|
|
||||||
_, err := conn.Write([]byte(pathname.String()))
|
|
||||||
if testing.Testing() && errors.Is(err, io.ErrClosedPipe) {
|
|
||||||
return a, nil
|
|
||||||
}
|
|
||||||
return a, errors.Join(err, conn.Close())
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// specialCancel is a message consisting of a single identifier referring
|
|
||||||
// to a curing artifact to be cancelled.
|
|
||||||
specialCancel = iota
|
|
||||||
// specialAbort requests for all pending cures to be aborted. It has no
|
|
||||||
// message body.
|
|
||||||
specialAbort
|
|
||||||
|
|
||||||
// remoteSpecial denotes a special message with custom layout.
|
|
||||||
remoteSpecial = math.MaxUint64
|
|
||||||
)
|
|
||||||
|
|
||||||
// writeSpecialHeader writes the header of a remoteSpecial message.
|
|
||||||
func writeSpecialHeader(conn net.Conn, kind uint64) error {
|
|
||||||
var sh [16]byte
|
|
||||||
binary.LittleEndian.PutUint64(sh[:], remoteSpecial)
|
|
||||||
binary.LittleEndian.PutUint64(sh[8:], kind)
|
|
||||||
if n, err := conn.Write(sh[:]); err != nil {
|
|
||||||
return err
|
|
||||||
} else if n != len(sh) {
|
|
||||||
return io.ErrShortWrite
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// cancelIdent reads an identifier from conn and cancels the corresponding cure.
|
|
||||||
func cancelIdent(
|
|
||||||
cache *pkg.Cache,
|
|
||||||
conn net.Conn,
|
|
||||||
) (*pkg.ID, bool, error) {
|
|
||||||
var ident pkg.ID
|
|
||||||
if _, err := io.ReadFull(conn, ident[:]); err != nil {
|
|
||||||
return nil, false, errors.Join(err, conn.Close())
|
|
||||||
}
|
|
||||||
ok := cache.Cancel(unique.Make(ident))
|
|
||||||
return &ident, ok, conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// serve services connections from a [net.UnixListener].
|
|
||||||
func serve(
|
|
||||||
ctx context.Context,
|
|
||||||
log *log.Logger,
|
|
||||||
cm *cache,
|
|
||||||
ul *net.UnixListener,
|
|
||||||
) error {
|
|
||||||
ul.SetUnlinkOnClose(true)
|
|
||||||
if cm.c == nil {
|
|
||||||
if err := cm.open(); err != nil {
|
|
||||||
return errors.Join(err, ul.Close())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
defer wg.Wait()
|
|
||||||
|
|
||||||
wg.Go(func() {
|
|
||||||
for {
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := ul.AcceptUnix()
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, os.ErrDeadlineExceeded) {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
wg.Go(func() {
|
|
||||||
done := make(chan struct{})
|
|
||||||
defer close(done)
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
_ = conn.SetDeadline(time.Now())
|
|
||||||
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if _err := conn.SetReadDeadline(daemonDeadline()); _err != nil {
|
|
||||||
log.Println(_err)
|
|
||||||
if _err = conn.Close(); _err != nil {
|
|
||||||
log.Println(_err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var word [8]byte
|
|
||||||
if _, _err := io.ReadFull(conn, word[:]); _err != nil {
|
|
||||||
log.Println(_err)
|
|
||||||
if _err = conn.Close(); _err != nil {
|
|
||||||
log.Println(_err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
flags := binary.LittleEndian.Uint64(word[:])
|
|
||||||
|
|
||||||
if flags == remoteSpecial {
|
|
||||||
if _, _err := io.ReadFull(conn, word[:]); _err != nil {
|
|
||||||
log.Println(_err)
|
|
||||||
if _err = conn.Close(); _err != nil {
|
|
||||||
log.Println(_err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch special := binary.LittleEndian.Uint64(word[:]); special {
|
|
||||||
default:
|
|
||||||
log.Printf("invalid special %d", special)
|
|
||||||
|
|
||||||
case specialCancel:
|
|
||||||
if id, ok, _err := cancelIdent(cm.c, conn); _err != nil {
|
|
||||||
log.Println(_err)
|
|
||||||
} else if !ok {
|
|
||||||
log.Println(
|
|
||||||
"attempting to cancel invalid artifact",
|
|
||||||
pkg.Encode(*id),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
log.Println(
|
|
||||||
"cancelled artifact",
|
|
||||||
pkg.Encode(*id),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
case specialAbort:
|
|
||||||
log.Println("aborting all pending cures")
|
|
||||||
cm.c.Abort()
|
|
||||||
if _err := conn.Close(); _err != nil {
|
|
||||||
log.Println(_err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if a, _err := cureFromIR(cm.c, conn, flags); _err != nil {
|
|
||||||
log.Println(_err)
|
|
||||||
} else {
|
|
||||||
log.Printf(
|
|
||||||
"fulfilled artifact %s",
|
|
||||||
pkg.Encode(cm.c.Ident(a).Value()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
<-ctx.Done()
|
|
||||||
if err := ul.SetDeadline(time.Now()); err != nil {
|
|
||||||
return errors.Join(err, ul.Close())
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
return ul.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// dial wraps [net.DialUnix] with a context.
|
|
||||||
func dial(ctx context.Context, addr *net.UnixAddr) (
|
|
||||||
done chan<- struct{},
|
|
||||||
conn *net.UnixConn,
|
|
||||||
err error,
|
|
||||||
) {
|
|
||||||
conn, err = net.DialUnix("unix", nil, addr)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
d := make(chan struct{})
|
|
||||||
done = d
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
_ = conn.SetDeadline(time.Now())
|
|
||||||
|
|
||||||
case <-d:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// cureRemote cures a [pkg.Artifact] on a daemon.
|
|
||||||
func cureRemote(
|
|
||||||
ctx context.Context,
|
|
||||||
addr *net.UnixAddr,
|
|
||||||
a pkg.Artifact,
|
|
||||||
flags uint64,
|
|
||||||
) (*check.Absolute, error) {
|
|
||||||
if flags == remoteSpecial {
|
|
||||||
return nil, syscall.EINVAL
|
|
||||||
}
|
|
||||||
|
|
||||||
done, conn, err := dial(ctx, addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer close(done)
|
|
||||||
|
|
||||||
if n, flagErr := conn.Write(binary.LittleEndian.AppendUint64(nil, flags)); flagErr != nil {
|
|
||||||
return nil, errors.Join(flagErr, conn.Close())
|
|
||||||
} else if n != 8 {
|
|
||||||
return nil, errors.Join(io.ErrShortWrite, conn.Close())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = pkg.NewIR().EncodeAll(conn, a); err != nil {
|
|
||||||
return nil, errors.Join(err, conn.Close())
|
|
||||||
} else if err = conn.CloseWrite(); err != nil {
|
|
||||||
return nil, errors.Join(err, conn.Close())
|
|
||||||
}
|
|
||||||
|
|
||||||
if flags&remoteNoReply != 0 {
|
|
||||||
return nil, conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
payload, recvErr := io.ReadAll(conn)
|
|
||||||
if err = errors.Join(recvErr, conn.Close()); err != nil {
|
|
||||||
if errors.Is(err, os.ErrDeadlineExceeded) {
|
|
||||||
if cancelErr := ctx.Err(); cancelErr != nil {
|
|
||||||
err = cancelErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(payload) > 0 && payload[0] == 0 {
|
|
||||||
return nil, errors.New(string(payload[1:]))
|
|
||||||
}
|
|
||||||
|
|
||||||
var p *check.Absolute
|
|
||||||
p, err = check.NewAbs(string(payload))
|
|
||||||
return p, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// cancelRemote cancels a [pkg.Artifact] curing on a daemon.
|
|
||||||
func cancelRemote(
|
|
||||||
ctx context.Context,
|
|
||||||
addr *net.UnixAddr,
|
|
||||||
a pkg.Artifact,
|
|
||||||
wait bool,
|
|
||||||
) error {
|
|
||||||
done, conn, err := dial(ctx, addr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer close(done)
|
|
||||||
|
|
||||||
if err = writeSpecialHeader(conn, specialCancel); err != nil {
|
|
||||||
return errors.Join(err, conn.Close())
|
|
||||||
}
|
|
||||||
|
|
||||||
var n int
|
|
||||||
id := pkg.NewIR().Ident(a).Value()
|
|
||||||
if n, err = conn.Write(id[:]); err != nil {
|
|
||||||
return errors.Join(err, conn.Close())
|
|
||||||
} else if n != len(id) {
|
|
||||||
return errors.Join(io.ErrShortWrite, conn.Close())
|
|
||||||
}
|
|
||||||
if wait {
|
|
||||||
if _, err = conn.Read(make([]byte, 1)); err == io.EOF {
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errors.Join(err, conn.Close())
|
|
||||||
}
|
|
||||||
|
|
||||||
// abortRemote aborts all [pkg.Artifact] curing on a daemon.
|
|
||||||
func abortRemote(
|
|
||||||
ctx context.Context,
|
|
||||||
addr *net.UnixAddr,
|
|
||||||
wait bool,
|
|
||||||
) error {
|
|
||||||
done, conn, err := dial(ctx, addr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer close(done)
|
|
||||||
|
|
||||||
err = writeSpecialHeader(conn, specialAbort)
|
|
||||||
if wait && err == nil {
|
|
||||||
if _, err = conn.Read(make([]byte, 1)); err == io.EOF {
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errors.Join(err, conn.Close())
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
"hakurei.app/internal/pkg"
|
|
||||||
"hakurei.app/message"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNoReply(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
if !daemonDeadline().IsZero() {
|
|
||||||
t.Fatal("daemonDeadline did not return the zero value")
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := pkg.Open(
|
|
||||||
t.Context(),
|
|
||||||
message.New(log.New(os.Stderr, "cir: ", 0)),
|
|
||||||
0, 0, 0,
|
|
||||||
check.MustAbs(t.TempDir()),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Open: error = %v", err)
|
|
||||||
}
|
|
||||||
defer c.Close()
|
|
||||||
|
|
||||||
client, server := net.Pipe()
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
defer close(done)
|
|
||||||
go func() {
|
|
||||||
<-t.Context().Done()
|
|
||||||
if _err := client.SetDeadline(time.Now()); _err != nil && !errors.Is(_err, io.ErrClosedPipe) {
|
|
||||||
panic(_err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if _err := c.EncodeAll(
|
|
||||||
client,
|
|
||||||
pkg.NewFile("check", []byte{0}),
|
|
||||||
); _err != nil {
|
|
||||||
panic(_err)
|
|
||||||
} else if _err = client.Close(); _err != nil {
|
|
||||||
panic(_err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
a, cureErr := cureFromIR(c, server, remoteNoReply)
|
|
||||||
if cureErr != nil {
|
|
||||||
t.Fatalf("cureFromIR: error = %v", cureErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
<-done
|
|
||||||
wantIdent := pkg.MustDecode("fiZf-ZY_Yq6qxJNrHbMiIPYCsGkUiKCRsZrcSELXTqZWtCnESlHmzV5ThhWWGGYG")
|
|
||||||
if gotIdent := c.Ident(a).Value(); gotIdent != wantIdent {
|
|
||||||
t.Errorf(
|
|
||||||
"cureFromIR: %s, want %s",
|
|
||||||
pkg.Encode(gotIdent), pkg.Encode(wantIdent),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDaemon(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
logger := log.New(&buf, "daemon: ", 0)
|
|
||||||
|
|
||||||
addr := net.UnixAddr{
|
|
||||||
Name: filepath.Join(t.TempDir(), "daemon"),
|
|
||||||
Net: "unix",
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(t.Context())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
cm := cache{
|
|
||||||
ctx: ctx,
|
|
||||||
msg: message.New(logger),
|
|
||||||
base: t.TempDir(),
|
|
||||||
}
|
|
||||||
defer cm.Close()
|
|
||||||
|
|
||||||
ul, err := net.ListenUnix("unix", &addr)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ListenUnix: error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
defer close(done)
|
|
||||||
if _err := serve(ctx, logger, &cm, ul); _err != nil {
|
|
||||||
panic(_err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err = cancelRemote(ctx, &addr, pkg.NewFile("nonexistent", nil), true); err != nil {
|
|
||||||
t.Fatalf("cancelRemote: error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = abortRemote(ctx, &addr, true); err != nil {
|
|
||||||
t.Fatalf("abortRemote: error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// keep this last for synchronisation
|
|
||||||
var p *check.Absolute
|
|
||||||
p, err = cureRemote(ctx, &addr, pkg.NewFile("check", []byte{0}), 0)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("cureRemote: error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
<-done
|
|
||||||
|
|
||||||
const want = "fiZf-ZY_Yq6qxJNrHbMiIPYCsGkUiKCRsZrcSELXTqZWtCnESlHmzV5ThhWWGGYG"
|
|
||||||
if got := filepath.Base(p.String()); got != want {
|
|
||||||
t.Errorf("cureRemote: %s, want %s", got, want)
|
|
||||||
}
|
|
||||||
|
|
||||||
wantLog := []string{
|
|
||||||
"",
|
|
||||||
"daemon: aborting all pending cures",
|
|
||||||
"daemon: attempting to cancel invalid artifact kQm9fmnCmXST1-MMmxzcau2oKZCXXrlZydo4PkeV5hO_2PKfeC8t98hrbV_ZZx_j",
|
|
||||||
"daemon: fulfilled artifact fiZf-ZY_Yq6qxJNrHbMiIPYCsGkUiKCRsZrcSELXTqZWtCnESlHmzV5ThhWWGGYG",
|
|
||||||
}
|
|
||||||
gotLog := strings.Split(buf.String(), "\n")
|
|
||||||
slices.Sort(gotLog)
|
|
||||||
if !slices.Equal(gotLog, wantLog) {
|
|
||||||
t.Errorf(
|
|
||||||
"serve: logged\n%s\nwant\n%s",
|
|
||||||
strings.Join(gotLog, "\n"), strings.Join(wantLog, "\n"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-114
@@ -1,114 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"hakurei.app/internal/pkg"
|
|
||||||
"hakurei.app/internal/rosa"
|
|
||||||
)
|
|
||||||
|
|
||||||
// commandInfo implements the info subcommand.
|
|
||||||
func commandInfo(
|
|
||||||
cm *cache,
|
|
||||||
args []string,
|
|
||||||
w io.Writer,
|
|
||||||
writeStatus bool,
|
|
||||||
r *rosa.Report,
|
|
||||||
) (err error) {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return errors.New("info requires at least 1 argument")
|
|
||||||
}
|
|
||||||
|
|
||||||
// recovered by HandleAccess
|
|
||||||
mustPrintln := func(a ...any) {
|
|
||||||
if _, _err := fmt.Fprintln(w, a...); _err != nil {
|
|
||||||
panic(_err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mustPrint := func(a ...any) {
|
|
||||||
if _, _err := fmt.Fprint(w, a...); _err != nil {
|
|
||||||
panic(_err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, name := range args {
|
|
||||||
if p, ok := rosa.ResolveName(name); !ok {
|
|
||||||
return fmt.Errorf("unknown artifact %q", name)
|
|
||||||
} else {
|
|
||||||
var suffix string
|
|
||||||
if version := rosa.Std.Version(p); version != rosa.Unversioned {
|
|
||||||
suffix += "-" + version
|
|
||||||
}
|
|
||||||
mustPrintln("name : " + name + suffix)
|
|
||||||
|
|
||||||
meta := rosa.GetMetadata(p)
|
|
||||||
mustPrintln("description : " + meta.Description)
|
|
||||||
if meta.Website != "" {
|
|
||||||
mustPrintln("website : " +
|
|
||||||
strings.TrimSuffix(meta.Website, "/"))
|
|
||||||
}
|
|
||||||
if len(meta.Dependencies) > 0 {
|
|
||||||
mustPrint("depends on :")
|
|
||||||
for _, d := range meta.Dependencies {
|
|
||||||
s := rosa.GetMetadata(d).Name
|
|
||||||
if version := rosa.Std.Version(d); version != rosa.Unversioned {
|
|
||||||
s += "-" + version
|
|
||||||
}
|
|
||||||
mustPrint(" " + s)
|
|
||||||
}
|
|
||||||
mustPrintln()
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusPrefix = "status : "
|
|
||||||
if writeStatus {
|
|
||||||
if r == nil {
|
|
||||||
var f io.ReadSeekCloser
|
|
||||||
err = cm.Do(func(cache *pkg.Cache) (err error) {
|
|
||||||
f, err = cache.OpenStatus(rosa.Std.Load(p))
|
|
||||||
return
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
mustPrintln(
|
|
||||||
statusPrefix + "not yet cured",
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mustPrint(statusPrefix)
|
|
||||||
_, err = io.Copy(w, f)
|
|
||||||
if err = errors.Join(err, f.Close()); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if err = cm.Do(func(cache *pkg.Cache) (err error) {
|
|
||||||
status, n := r.ArtifactOf(cache.Ident(rosa.Std.Load(p)))
|
|
||||||
if status == nil {
|
|
||||||
mustPrintln(
|
|
||||||
statusPrefix + "not in report",
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
mustPrintln("size :", n)
|
|
||||||
mustPrint(statusPrefix)
|
|
||||||
if _, err = w.Write(status); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if i != len(args)-1 {
|
|
||||||
mustPrintln()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"testing"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"hakurei.app/internal/pkg"
|
|
||||||
"hakurei.app/internal/rosa"
|
|
||||||
"hakurei.app/message"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestInfo(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
args []string
|
|
||||||
status map[string]string
|
|
||||||
report string
|
|
||||||
want string
|
|
||||||
wantErr any
|
|
||||||
}{
|
|
||||||
{"qemu", []string{"qemu"}, nil, "", `
|
|
||||||
name : qemu-` + rosa.Std.Version(rosa.QEMU) + `
|
|
||||||
description : a generic and open source machine emulator and virtualizer
|
|
||||||
website : https://www.qemu.org
|
|
||||||
depends on : glib-` + rosa.Std.Version(rosa.GLib) + ` zstd-` + rosa.Std.Version(rosa.Zstd) + `
|
|
||||||
`, nil},
|
|
||||||
|
|
||||||
{"multi", []string{"hakurei", "hakurei-dist"}, nil, "", `
|
|
||||||
name : hakurei-` + rosa.Std.Version(rosa.Hakurei) + `
|
|
||||||
description : low-level userspace tooling for Rosa OS
|
|
||||||
website : https://hakurei.app
|
|
||||||
|
|
||||||
name : hakurei-dist-` + rosa.Std.Version(rosa.HakureiDist) + `
|
|
||||||
description : low-level userspace tooling for Rosa OS (distribution tarball)
|
|
||||||
website : https://hakurei.app
|
|
||||||
`, nil},
|
|
||||||
|
|
||||||
{"nonexistent", []string{"zlib", "\x00"}, nil, "", `
|
|
||||||
name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
|
|
||||||
description : lossless data-compression library
|
|
||||||
website : https://zlib.net
|
|
||||||
|
|
||||||
`, fmt.Errorf("unknown artifact %q", "\x00")},
|
|
||||||
|
|
||||||
{"status cache", []string{"zlib", "zstd"}, map[string]string{
|
|
||||||
"zstd": "internal/pkg (amd64) on satori\n",
|
|
||||||
"hakurei": "internal/pkg (amd64) on satori\n\n",
|
|
||||||
}, "", `
|
|
||||||
name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
|
|
||||||
description : lossless data-compression library
|
|
||||||
website : https://zlib.net
|
|
||||||
status : not yet cured
|
|
||||||
|
|
||||||
name : zstd-` + rosa.Std.Version(rosa.Zstd) + `
|
|
||||||
description : a fast compression algorithm
|
|
||||||
website : https://facebook.github.io/zstd
|
|
||||||
status : internal/pkg (amd64) on satori
|
|
||||||
`, nil},
|
|
||||||
|
|
||||||
{"status cache perm", []string{"zlib"}, map[string]string{
|
|
||||||
"zlib": "\x00",
|
|
||||||
}, "", `
|
|
||||||
name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
|
|
||||||
description : lossless data-compression library
|
|
||||||
website : https://zlib.net
|
|
||||||
`, func(cm *cache) error {
|
|
||||||
return &os.PathError{
|
|
||||||
Op: "open",
|
|
||||||
Path: filepath.Join(cm.base, "status", pkg.Encode(cm.c.Ident(rosa.Std.Load(rosa.Zlib)).Value())),
|
|
||||||
Err: syscall.EACCES,
|
|
||||||
}
|
|
||||||
}},
|
|
||||||
|
|
||||||
{"status report", []string{"zlib"}, nil, strings.Repeat("\x00", len(pkg.Checksum{})+8), `
|
|
||||||
name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
|
|
||||||
description : lossless data-compression library
|
|
||||||
website : https://zlib.net
|
|
||||||
status : not in report
|
|
||||||
`, nil},
|
|
||||||
}
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
cm *cache
|
|
||||||
buf strings.Builder
|
|
||||||
r *rosa.Report
|
|
||||||
)
|
|
||||||
|
|
||||||
if tc.status != nil || tc.report != "" {
|
|
||||||
cm = &cache{
|
|
||||||
ctx: context.Background(),
|
|
||||||
msg: message.New(log.New(os.Stderr, "info: ", 0)),
|
|
||||||
base: t.TempDir(),
|
|
||||||
}
|
|
||||||
defer cm.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
if tc.report != "" {
|
|
||||||
pathname := filepath.Join(t.TempDir(), "report")
|
|
||||||
err := os.WriteFile(
|
|
||||||
pathname,
|
|
||||||
unsafe.Slice(unsafe.StringData(tc.report), len(tc.report)),
|
|
||||||
0400,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err = rosa.OpenReport(pathname)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err = r.Close(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
if tc.status != nil {
|
|
||||||
for name, status := range tc.status {
|
|
||||||
p, ok := rosa.ResolveName(name)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("invalid name %q", name)
|
|
||||||
}
|
|
||||||
perm := os.FileMode(0400)
|
|
||||||
if status == "\x00" {
|
|
||||||
perm = 0
|
|
||||||
}
|
|
||||||
if err := cm.Do(func(cache *pkg.Cache) error {
|
|
||||||
return os.WriteFile(filepath.Join(
|
|
||||||
cm.base,
|
|
||||||
"status",
|
|
||||||
pkg.Encode(cache.Ident(rosa.Std.Load(p)).Value()),
|
|
||||||
), unsafe.Slice(unsafe.StringData(status), len(status)), perm)
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatalf("Do: error = %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var wantErr error
|
|
||||||
switch c := tc.wantErr.(type) {
|
|
||||||
case error:
|
|
||||||
wantErr = c
|
|
||||||
case func(cm *cache) error:
|
|
||||||
wantErr = c(cm)
|
|
||||||
default:
|
|
||||||
if tc.wantErr != nil {
|
|
||||||
t.Fatalf("invalid wantErr %#v", tc.wantErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := commandInfo(
|
|
||||||
cm,
|
|
||||||
tc.args,
|
|
||||||
&buf,
|
|
||||||
cm != nil,
|
|
||||||
r,
|
|
||||||
); !reflect.DeepEqual(err, wantErr) {
|
|
||||||
t.Fatalf("commandInfo: error = %v, want %v", err, wantErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := buf.String(); got != strings.TrimPrefix(tc.want, "\n") {
|
|
||||||
t.Errorf("commandInfo:\n%s\nwant\n%s", got, tc.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
// Package pkgserver implements the package metadata service backend.
|
|
||||||
package pkgserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"hakurei.app/internal/info"
|
|
||||||
"hakurei.app/internal/rosa"
|
|
||||||
)
|
|
||||||
|
|
||||||
// for lazy initialisation of serveInfo
|
|
||||||
var (
|
|
||||||
infoPayload struct {
|
|
||||||
// Current package count.
|
|
||||||
Count int `json:"count"`
|
|
||||||
// Hakurei version, set at link time.
|
|
||||||
HakureiVersion string `json:"hakurei_version"`
|
|
||||||
}
|
|
||||||
infoPayloadOnce sync.Once
|
|
||||||
)
|
|
||||||
|
|
||||||
// handleInfo writes constant system information.
|
|
||||||
func handleInfo(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
infoPayloadOnce.Do(func() {
|
|
||||||
infoPayload.Count = int(rosa.PresetUnexportedStart)
|
|
||||||
infoPayload.HakureiVersion = info.Version()
|
|
||||||
})
|
|
||||||
// TODO(mae): cache entire response if no additional fields are planned
|
|
||||||
writeAPIPayload(w, infoPayload)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newStatusHandler returns a [http.HandlerFunc] that offers status files for
|
|
||||||
// viewing or download, if available.
|
|
||||||
func (index *packageIndex) newStatusHandler(disposition bool) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
m, ok := index.names[path.Base(r.URL.Path)]
|
|
||||||
if !ok || !m.HasReport {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType := "text/plain; charset=utf-8"
|
|
||||||
if disposition {
|
|
||||||
contentType = "application/octet-stream"
|
|
||||||
|
|
||||||
// quoting like this is unsound, but okay, because metadata is hardcoded
|
|
||||||
contentDisposition := `attachment; filename="`
|
|
||||||
contentDisposition += m.Name + "-"
|
|
||||||
if m.Version != "" {
|
|
||||||
contentDisposition += m.Version + "-"
|
|
||||||
}
|
|
||||||
contentDisposition += m.ids + `.log"`
|
|
||||||
w.Header().Set("Content-Disposition", contentDisposition)
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", contentType)
|
|
||||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
||||||
if err := func() (err error) {
|
|
||||||
defer index.handleAccess(&err)()
|
|
||||||
_, err = w.Write(m.status)
|
|
||||||
return
|
|
||||||
}(); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
http.Error(
|
|
||||||
w, "cannot deliver status, contact maintainers",
|
|
||||||
http.StatusInternalServerError,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleGet writes a slice of metadata with specified order.
|
|
||||||
func (index *packageIndex) handleGet(w http.ResponseWriter, r *http.Request) {
|
|
||||||
q := r.URL.Query()
|
|
||||||
limit, err := strconv.Atoi(q.Get("limit"))
|
|
||||||
if err != nil || limit > 100 || limit < 1 {
|
|
||||||
http.Error(
|
|
||||||
w, "limit must be an integer between 1 and 100",
|
|
||||||
http.StatusBadRequest,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
i, err := strconv.Atoi(q.Get("index"))
|
|
||||||
if err != nil || i >= len(index.sorts[0]) || i < 0 {
|
|
||||||
http.Error(
|
|
||||||
w, "index must be an integer between 0 and "+
|
|
||||||
strconv.Itoa(int(rosa.PresetUnexportedStart-1)),
|
|
||||||
http.StatusBadRequest,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sort, err := strconv.Atoi(q.Get("sort"))
|
|
||||||
if err != nil || sort >= len(index.sorts) || sort < 0 {
|
|
||||||
http.Error(
|
|
||||||
w, "sort must be an integer between 0 and "+
|
|
||||||
strconv.Itoa(sortOrderEnd),
|
|
||||||
http.StatusBadRequest,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
values := index.sorts[sort][i:min(i+limit, len(index.sorts[sort]))]
|
|
||||||
writeAPIPayload(w, &struct {
|
|
||||||
Values []*metadata `json:"values"`
|
|
||||||
}{values})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (index *packageIndex) handleSearch(w http.ResponseWriter, r *http.Request) {
|
|
||||||
q := r.URL.Query()
|
|
||||||
limit, err := strconv.Atoi(q.Get("limit"))
|
|
||||||
if err != nil || limit > 100 || limit < 1 {
|
|
||||||
http.Error(
|
|
||||||
w, "limit must be an integer between 1 and 100",
|
|
||||||
http.StatusBadRequest,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
i, err := strconv.Atoi(q.Get("index"))
|
|
||||||
if err != nil || i >= len(index.sorts[0]) || i < 0 {
|
|
||||||
http.Error(
|
|
||||||
w, "index must be an integer between 0 and "+
|
|
||||||
strconv.Itoa(int(rosa.PresetUnexportedStart-1)),
|
|
||||||
http.StatusBadRequest,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
search, err := url.QueryUnescape(q.Get("search"))
|
|
||||||
if len(search) > 100 || err != nil {
|
|
||||||
http.Error(
|
|
||||||
w, "search must be a string between 0 and 100 characters long",
|
|
||||||
http.StatusBadRequest,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
desc := q.Get("desc") == "true"
|
|
||||||
n, res, err := index.performSearchQuery(limit, i, search, desc)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
writeAPIPayload(w, &struct {
|
|
||||||
Count int `json:"count"`
|
|
||||||
Values []searchResult `json:"values"`
|
|
||||||
}{n, res})
|
|
||||||
}
|
|
||||||
|
|
||||||
// apiVersion is the name of the current API revision, as part of the pattern.
|
|
||||||
const apiVersion = "v1"
|
|
||||||
|
|
||||||
// registerAPI registers API handler functions.
|
|
||||||
func (index *packageIndex) registerAPI(mux *http.ServeMux) {
|
|
||||||
mux.HandleFunc("GET /api/"+apiVersion+"/info", handleInfo)
|
|
||||||
mux.HandleFunc("GET /api/"+apiVersion+"/get", index.handleGet)
|
|
||||||
mux.HandleFunc("GET /api/"+apiVersion+"/search", index.handleSearch)
|
|
||||||
mux.HandleFunc("GET /api/"+apiVersion+"/status/", index.newStatusHandler(false))
|
|
||||||
mux.HandleFunc("GET /status/", index.newStatusHandler(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register arranges for mux to service API requests.
|
|
||||||
func Register(ctx context.Context, mux *http.ServeMux, report *rosa.Report) error {
|
|
||||||
var index packageIndex
|
|
||||||
index.search = make(searchCache)
|
|
||||||
if err := index.populate(report); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ticker := time.NewTicker(1 * time.Minute)
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
ticker.Stop()
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
index.search.clean()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
index.registerAPI(mux)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeAPIPayload sets headers common to API responses and encodes payload as
|
|
||||||
// JSON for the response body.
|
|
||||||
func writeAPIPayload(w http.ResponseWriter, payload any) {
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
||||||
w.Header().Set("Pragma", "no-cache")
|
|
||||||
w.Header().Set("Expires", "0")
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
http.Error(
|
|
||||||
w, "cannot encode payload, contact maintainers",
|
|
||||||
http.StatusInternalServerError,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
package pkgserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"hakurei.app/internal/info"
|
|
||||||
"hakurei.app/internal/rosa"
|
|
||||||
)
|
|
||||||
|
|
||||||
// prefix is prepended to every API path.
|
|
||||||
const prefix = "/api/" + apiVersion + "/"
|
|
||||||
|
|
||||||
func TestAPIInfo(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
handleInfo(w, httptest.NewRequestWithContext(
|
|
||||||
t.Context(),
|
|
||||||
http.MethodGet,
|
|
||||||
prefix+"info",
|
|
||||||
nil,
|
|
||||||
))
|
|
||||||
|
|
||||||
resp := w.Result()
|
|
||||||
checkStatus(t, resp, http.StatusOK)
|
|
||||||
checkAPIHeader(t, w.Header())
|
|
||||||
|
|
||||||
checkPayload(t, resp, struct {
|
|
||||||
Count int `json:"count"`
|
|
||||||
HakureiVersion string `json:"hakurei_version"`
|
|
||||||
}{int(rosa.PresetUnexportedStart), info.Version()})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAPIGet(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
const target = prefix + "get"
|
|
||||||
|
|
||||||
index := newIndex(t)
|
|
||||||
newRequest := func(suffix string) *httptest.ResponseRecorder {
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
index.handleGet(w, httptest.NewRequestWithContext(
|
|
||||||
t.Context(),
|
|
||||||
http.MethodGet,
|
|
||||||
target+suffix,
|
|
||||||
nil,
|
|
||||||
))
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
|
|
||||||
checkValidate := func(t *testing.T, suffix string, vmin, vmax int, wantErr string) {
|
|
||||||
t.Run("invalid", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
w := newRequest("?" + suffix + "=invalid")
|
|
||||||
resp := w.Result()
|
|
||||||
checkError(t, resp, wantErr, http.StatusBadRequest)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("min", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
w := newRequest("?" + suffix + "=" + strconv.Itoa(vmin-1))
|
|
||||||
resp := w.Result()
|
|
||||||
checkError(t, resp, wantErr, http.StatusBadRequest)
|
|
||||||
|
|
||||||
w = newRequest("?" + suffix + "=" + strconv.Itoa(vmin))
|
|
||||||
resp = w.Result()
|
|
||||||
checkStatus(t, resp, http.StatusOK)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("max", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
w := newRequest("?" + suffix + "=" + strconv.Itoa(vmax+1))
|
|
||||||
resp := w.Result()
|
|
||||||
checkError(t, resp, wantErr, http.StatusBadRequest)
|
|
||||||
|
|
||||||
w = newRequest("?" + suffix + "=" + strconv.Itoa(vmax))
|
|
||||||
resp = w.Result()
|
|
||||||
checkStatus(t, resp, http.StatusOK)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("limit", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
checkValidate(
|
|
||||||
t, "index=0&sort=0&limit", 1, 100,
|
|
||||||
"limit must be an integer between 1 and 100",
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("index", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
checkValidate(
|
|
||||||
t, "limit=1&sort=0&index", 0, int(rosa.PresetUnexportedStart-1),
|
|
||||||
"index must be an integer between 0 and "+strconv.Itoa(int(rosa.PresetUnexportedStart-1)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("sort", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
checkValidate(
|
|
||||||
t, "index=0&limit=1&sort", 0, int(sortOrderEnd),
|
|
||||||
"sort must be an integer between 0 and "+strconv.Itoa(int(sortOrderEnd)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
checkWithSuffix := func(name, suffix string, want []*metadata) {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
w := newRequest(suffix)
|
|
||||||
resp := w.Result()
|
|
||||||
checkStatus(t, resp, http.StatusOK)
|
|
||||||
checkAPIHeader(t, w.Header())
|
|
||||||
checkPayloadFunc(t, resp, func(got *struct {
|
|
||||||
Values []*metadata `json:"values"`
|
|
||||||
}) bool {
|
|
||||||
return slices.EqualFunc(got.Values, want, func(a, b *metadata) bool {
|
|
||||||
return (a.Version == b.Version ||
|
|
||||||
a.Version == rosa.Unversioned ||
|
|
||||||
b.Version == rosa.Unversioned) &&
|
|
||||||
a.HasReport == b.HasReport &&
|
|
||||||
a.Name == b.Name &&
|
|
||||||
a.Description == b.Description &&
|
|
||||||
a.Website == b.Website
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
checkWithSuffix("declarationAscending", "?limit=2&index=1&sort=0", []*metadata{
|
|
||||||
{
|
|
||||||
Metadata: rosa.GetMetadata(1),
|
|
||||||
Version: rosa.Std.Version(1),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Metadata: rosa.GetMetadata(2),
|
|
||||||
Version: rosa.Std.Version(2),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
checkWithSuffix("declarationAscending offset", "?limit=3&index=5&sort=0", []*metadata{
|
|
||||||
{
|
|
||||||
Metadata: rosa.GetMetadata(5),
|
|
||||||
Version: rosa.Std.Version(5),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Metadata: rosa.GetMetadata(6),
|
|
||||||
Version: rosa.Std.Version(6),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Metadata: rosa.GetMetadata(7),
|
|
||||||
Version: rosa.Std.Version(7),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
checkWithSuffix("declarationDescending", "?limit=3&index=0&sort=1", []*metadata{
|
|
||||||
{
|
|
||||||
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 1),
|
|
||||||
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 1),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 2),
|
|
||||||
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 2),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 3),
|
|
||||||
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 3),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
checkWithSuffix("declarationDescending offset", "?limit=1&index=37&sort=1", []*metadata{
|
|
||||||
{
|
|
||||||
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 38),
|
|
||||||
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 38),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
package pkgserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"cmp"
|
|
||||||
"errors"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"hakurei.app/internal/pkg"
|
|
||||||
"hakurei.app/internal/rosa"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
declarationAscending = iota
|
|
||||||
declarationDescending
|
|
||||||
nameAscending
|
|
||||||
nameDescending
|
|
||||||
sizeAscending
|
|
||||||
sizeDescending
|
|
||||||
|
|
||||||
sortOrderEnd = iota - 1
|
|
||||||
)
|
|
||||||
|
|
||||||
// packageIndex refers to metadata by name and various sort orders.
|
|
||||||
type packageIndex struct {
|
|
||||||
sorts [sortOrderEnd + 1][rosa.PresetUnexportedStart]*metadata
|
|
||||||
names map[string]*metadata
|
|
||||||
search searchCache
|
|
||||||
// Taken from [rosa.Report] if available.
|
|
||||||
handleAccess func(*error) func()
|
|
||||||
}
|
|
||||||
|
|
||||||
// metadata holds [rosa.Metadata] extended with additional information.
|
|
||||||
type metadata struct {
|
|
||||||
p rosa.PArtifact
|
|
||||||
*rosa.Metadata
|
|
||||||
|
|
||||||
// Populated via [rosa.Toolchain.Version], [rosa.Unversioned] is equivalent
|
|
||||||
// to the zero value. Otherwise, the zero value is invalid.
|
|
||||||
Version string `json:"version,omitempty"`
|
|
||||||
// Output data size, available if present in report.
|
|
||||||
Size int64 `json:"size,omitempty"`
|
|
||||||
// Whether the underlying [pkg.Artifact] is present in the report.
|
|
||||||
HasReport bool `json:"report"`
|
|
||||||
|
|
||||||
// Ident string encoded ahead of time.
|
|
||||||
ids string
|
|
||||||
// Backed by [rosa.Report], access must be prepared by HandleAccess.
|
|
||||||
status []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// populate deterministically populates packageIndex, optionally with a report.
|
|
||||||
func (index *packageIndex) populate(report *rosa.Report) (err error) {
|
|
||||||
if report != nil {
|
|
||||||
defer report.HandleAccess(&err)()
|
|
||||||
index.handleAccess = report.HandleAccess
|
|
||||||
}
|
|
||||||
|
|
||||||
var work [rosa.PresetUnexportedStart]*metadata
|
|
||||||
index.names = make(map[string]*metadata)
|
|
||||||
ir := pkg.NewIR()
|
|
||||||
for p := range rosa.PresetUnexportedStart {
|
|
||||||
m := metadata{
|
|
||||||
p: p,
|
|
||||||
|
|
||||||
Metadata: rosa.GetMetadata(p),
|
|
||||||
Version: rosa.Std.Version(p),
|
|
||||||
}
|
|
||||||
if m.Version == "" {
|
|
||||||
return errors.New("invalid version from " + m.Name)
|
|
||||||
}
|
|
||||||
if m.Version == rosa.Unversioned {
|
|
||||||
m.Version = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if report != nil {
|
|
||||||
id := ir.Ident(rosa.Std.Load(p))
|
|
||||||
m.ids = pkg.Encode(id.Value())
|
|
||||||
m.status, m.Size = report.ArtifactOf(id)
|
|
||||||
m.HasReport = m.Size >= 0
|
|
||||||
}
|
|
||||||
|
|
||||||
work[p] = &m
|
|
||||||
index.names[m.Name] = &m
|
|
||||||
}
|
|
||||||
|
|
||||||
index.sorts[declarationAscending] = work
|
|
||||||
index.sorts[declarationDescending] = work
|
|
||||||
slices.Reverse(index.sorts[declarationDescending][:])
|
|
||||||
|
|
||||||
index.sorts[nameAscending] = work
|
|
||||||
slices.SortFunc(index.sorts[nameAscending][:], func(a, b *metadata) int {
|
|
||||||
return strings.Compare(a.Name, b.Name)
|
|
||||||
})
|
|
||||||
index.sorts[nameDescending] = index.sorts[nameAscending]
|
|
||||||
slices.Reverse(index.sorts[nameDescending][:])
|
|
||||||
|
|
||||||
index.sorts[sizeAscending] = work
|
|
||||||
slices.SortFunc(index.sorts[sizeAscending][:], func(a, b *metadata) int {
|
|
||||||
return cmp.Compare(a.Size, b.Size)
|
|
||||||
})
|
|
||||||
index.sorts[sizeDescending] = index.sorts[sizeAscending]
|
|
||||||
slices.Reverse(index.sorts[sizeDescending][:])
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package pkgserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// newIndex returns the address of a newly populated packageIndex.
|
|
||||||
func newIndex(t *testing.T) *packageIndex {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
var index packageIndex
|
|
||||||
if err := index.populate(nil); err != nil {
|
|
||||||
t.Fatalf("populate: error = %v", err)
|
|
||||||
}
|
|
||||||
return &index
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkStatus checks response status code.
|
|
||||||
func checkStatus(t *testing.T, resp *http.Response, want int) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
if resp.StatusCode != want {
|
|
||||||
t.Errorf(
|
|
||||||
"StatusCode: %s, want %s",
|
|
||||||
http.StatusText(resp.StatusCode),
|
|
||||||
http.StatusText(want),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkHeader checks the value of a header entry.
|
|
||||||
func checkHeader(t *testing.T, h http.Header, key, want string) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
if got := h.Get(key); got != want {
|
|
||||||
t.Errorf("%s: %q, want %q", key, got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkAPIHeader checks common entries set for API endpoints.
|
|
||||||
func checkAPIHeader(t *testing.T, h http.Header) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
checkHeader(t, h, "Content-Type", "application/json; charset=utf-8")
|
|
||||||
checkHeader(t, h, "Cache-Control", "no-cache, no-store, must-revalidate")
|
|
||||||
checkHeader(t, h, "Pragma", "no-cache")
|
|
||||||
checkHeader(t, h, "Expires", "0")
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkPayloadFunc checks the JSON response of an API endpoint by passing it to f.
|
|
||||||
func checkPayloadFunc[T any](
|
|
||||||
t *testing.T,
|
|
||||||
resp *http.Response,
|
|
||||||
f func(got *T) bool,
|
|
||||||
) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
var got T
|
|
||||||
r := io.Reader(resp.Body)
|
|
||||||
if testing.Verbose() {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
r = io.TeeReader(r, &buf)
|
|
||||||
defer func() { t.Helper(); t.Log(buf.String()) }()
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(r).Decode(&got); err != nil {
|
|
||||||
t.Fatalf("Decode: error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !f(&got) {
|
|
||||||
t.Errorf("Body: %#v", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkPayload checks the JSON response of an API endpoint.
|
|
||||||
func checkPayload[T any](t *testing.T, resp *http.Response, want T) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
checkPayloadFunc(t, resp, func(got *T) bool {
|
|
||||||
return reflect.DeepEqual(got, &want)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkError(t *testing.T, resp *http.Response, error string, code int) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
checkStatus(t, resp, code)
|
|
||||||
if got, _ := io.ReadAll(resp.Body); string(got) != fmt.Sprintln(error) {
|
|
||||||
t.Errorf("Body: %q, want %q", string(got), error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package pkgserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"cmp"
|
|
||||||
"maps"
|
|
||||||
"regexp"
|
|
||||||
"slices"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type searchCache map[string]searchCacheEntry
|
|
||||||
type searchResult struct {
|
|
||||||
NameIndices [][]int `json:"name_matches"`
|
|
||||||
DescIndices [][]int `json:"desc_matches,omitempty"`
|
|
||||||
Score float64 `json:"score"`
|
|
||||||
*metadata
|
|
||||||
}
|
|
||||||
type searchCacheEntry struct {
|
|
||||||
query string
|
|
||||||
results []searchResult
|
|
||||||
expiry time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (index *packageIndex) performSearchQuery(limit int, i int, search string, desc bool) (int, []searchResult, error) {
|
|
||||||
query := search
|
|
||||||
if desc {
|
|
||||||
query += ";withDesc"
|
|
||||||
}
|
|
||||||
entry, ok := index.search[query]
|
|
||||||
if ok && len(entry.results) > 0 {
|
|
||||||
return len(entry.results), entry.results[min(i, len(entry.results)-1):min(i+limit, len(entry.results))], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
regex, err := regexp.Compile(search)
|
|
||||||
if err != nil {
|
|
||||||
return 0, make([]searchResult, 0), err
|
|
||||||
}
|
|
||||||
res := make([]searchResult, 0)
|
|
||||||
for p := range maps.Values(index.names) {
|
|
||||||
nameIndices := regex.FindAllIndex([]byte(p.Name), -1)
|
|
||||||
var descIndices [][]int = nil
|
|
||||||
if desc {
|
|
||||||
descIndices = regex.FindAllIndex([]byte(p.Description), -1)
|
|
||||||
}
|
|
||||||
if nameIndices == nil && descIndices == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
score := float64(indexsum(nameIndices)) / (float64(len(nameIndices)) + 1)
|
|
||||||
if desc {
|
|
||||||
score += float64(indexsum(descIndices)) / (float64(len(descIndices)) + 1) / 10.0
|
|
||||||
}
|
|
||||||
res = append(res, searchResult{
|
|
||||||
NameIndices: nameIndices,
|
|
||||||
DescIndices: descIndices,
|
|
||||||
Score: score,
|
|
||||||
metadata: p,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
slices.SortFunc(res[:], func(a, b searchResult) int { return -cmp.Compare(a.Score, b.Score) })
|
|
||||||
expiry := time.Now().Add(1 * time.Minute)
|
|
||||||
entry = searchCacheEntry{
|
|
||||||
query: search,
|
|
||||||
results: res,
|
|
||||||
expiry: expiry,
|
|
||||||
}
|
|
||||||
index.search[query] = entry
|
|
||||||
|
|
||||||
return len(res), res[i:min(i+limit, len(entry.results))], nil
|
|
||||||
}
|
|
||||||
func (s *searchCache) clean() {
|
|
||||||
maps.DeleteFunc(*s, func(_ string, v searchCacheEntry) bool {
|
|
||||||
return v.expiry.Before(time.Now())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
func indexsum(in [][]int) int {
|
|
||||||
sum := 0
|
|
||||||
for i := 0; i < len(in); i++ {
|
|
||||||
sum += in[i][1] - in[i][0]
|
|
||||||
}
|
|
||||||
return sum
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
<title>Hakurei PkgServer</title>
|
|
||||||
<script src="index.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Hakurei PkgServer</h1>
|
|
||||||
<div class="top-controls" id="top-controls-regular">
|
|
||||||
<p>Showing entries <span id="entry-counter"></span>.</p>
|
|
||||||
<span id="search-bar">
|
|
||||||
<label for="search">Search: </label>
|
|
||||||
<input type="text" name="search" id="search"/>
|
|
||||||
<button onclick="doSearch()">Find</button>
|
|
||||||
<label for="include-desc">Include descriptions: </label>
|
|
||||||
<input type="checkbox" name="include-desc" id="include-desc" checked/>
|
|
||||||
</span>
|
|
||||||
<div><label for="count">Entries per page: </label><select name="count" id="count">
|
|
||||||
<option value="10">10</option>
|
|
||||||
<option value="20">20</option>
|
|
||||||
<option value="30">30</option>
|
|
||||||
<option value="50">50</option>
|
|
||||||
</select></div>
|
|
||||||
<div><label for="sort">Sort by: </label><select name="sort" id="sort">
|
|
||||||
<option value="0">Definition (ascending)</option>
|
|
||||||
<option value="1">Definition (descending)</option>
|
|
||||||
<option value="2">Name (ascending)</option>
|
|
||||||
<option value="3">Name (descending)</option>
|
|
||||||
<option value="4">Size (ascending)</option>
|
|
||||||
<option value="5">Size (descending)</option>
|
|
||||||
</select></div>
|
|
||||||
</div>
|
|
||||||
<div class="top-controls" id="search-top-controls" hidden>
|
|
||||||
<p>Showing search results <span id="search-entry-counter"></span> for query "<span id="search-query"></span>".</p>
|
|
||||||
<button onclick="exitSearch()">Back</button>
|
|
||||||
<div><label for="search-count">Entries per page: </label><select name="search-count" id="search-count">
|
|
||||||
<option value="10">10</option>
|
|
||||||
<option value="20">20</option>
|
|
||||||
<option value="30">30</option>
|
|
||||||
<option value="50">50</option>
|
|
||||||
</select></div>
|
|
||||||
<p>Sorted by best match</p>
|
|
||||||
</div>
|
|
||||||
<div class="page-controls"><a href="javascript:prevPage()">« Previous</a> <input type="text" class="page-number" value="1"/> <a href="javascript:nextPage()">Next »</a></div>
|
|
||||||
<table id="pkg-list">
|
|
||||||
<tr><td>Loading...</td></tr>
|
|
||||||
</table>
|
|
||||||
<div class="page-controls"><a href="javascript:prevPage()">« Previous</a> <input type="text" class="page-number" value="1"/> <a href="javascript:nextPage()">Next »</a></div>
|
|
||||||
<footer>
|
|
||||||
<p>©<a href="https://hakurei.app/">Hakurei</a> (<span id="hakurei-version">unknown</span>). Licensed under the MIT license.</p>
|
|
||||||
</footer>
|
|
||||||
<script>main();</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
interface PackageIndexEntry {
|
|
||||||
name: string
|
|
||||||
size?: number
|
|
||||||
description?: string
|
|
||||||
website?: string
|
|
||||||
version?: string
|
|
||||||
report?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
function entryToHTML(entry: PackageIndexEntry | SearchResult): HTMLTableRowElement {
|
|
||||||
let v = entry.version != null ? `<span>${escapeHtml(entry.version)}</span>` : ""
|
|
||||||
let s = entry.size != null && entry.size > 0 ? `<p>Size: ${toByteSizeString(entry.size)} (${entry.size})</p>` : ""
|
|
||||||
let n: string
|
|
||||||
let d: string
|
|
||||||
if ('name_matches' in entry) {
|
|
||||||
n = `<h2>${nameMatches(entry as SearchResult)} ${v}</h2>`
|
|
||||||
} else {
|
|
||||||
n = `<h2>${escapeHtml(entry.name)} ${v}</h2>`
|
|
||||||
}
|
|
||||||
if ('desc_matches' in entry && STATE.getIncludeDescriptions()) {
|
|
||||||
d = descMatches(entry as SearchResult)
|
|
||||||
} else {
|
|
||||||
d = (entry as PackageIndexEntry).description != null ? `<p>${escapeHtml((entry as PackageIndexEntry).description)}</p>` : ""
|
|
||||||
}
|
|
||||||
let w = entry.website != null ? `<a href="${encodeURI(entry.website)}">Website</a>` : ""
|
|
||||||
let r = entry.report ? `Log (<a href=\"${encodeURI('/api/v1/status/' + entry.name)}\">View</a> | <a href=\"${encodeURI('/status/' + entry.name)}\">Download</a>)` : ""
|
|
||||||
let row = <HTMLTableRowElement>(document.createElement('tr'))
|
|
||||||
row.innerHTML = `<td>
|
|
||||||
${n}
|
|
||||||
${d}
|
|
||||||
${s}
|
|
||||||
${w}
|
|
||||||
${r}
|
|
||||||
</td>`
|
|
||||||
return row
|
|
||||||
}
|
|
||||||
|
|
||||||
function nameMatches(sr: SearchResult): string {
|
|
||||||
return markMatches(sr.name, sr.name_matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
function descMatches(sr: SearchResult): string {
|
|
||||||
return markMatches(sr.description!, sr.desc_matches)
|
|
||||||
}
|
|
||||||
|
|
||||||
function markMatches(str: string, indices: [number, number][]): string {
|
|
||||||
if (indices == null) {
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
let out: string = ""
|
|
||||||
let j = 0
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
if (j < indices.length) {
|
|
||||||
if (i === indices[j][0]) {
|
|
||||||
out += `<mark>${escapeHtmlChar(str[i])}`
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (i === indices[j][1]) {
|
|
||||||
out += `</mark>${escapeHtmlChar(str[i])}`
|
|
||||||
j++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out += escapeHtmlChar(str[i])
|
|
||||||
}
|
|
||||||
if (indices[j] !== undefined) {
|
|
||||||
out += "</mark>"
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
function toByteSizeString(bytes: number): string {
|
|
||||||
if (bytes == null) return `unspecified`
|
|
||||||
if (bytes < 1024) return `${bytes}B`
|
|
||||||
if (bytes < Math.pow(1024, 2)) return `${(bytes / 1024).toFixed(2)}kiB`
|
|
||||||
if (bytes < Math.pow(1024, 3)) return `${(bytes / Math.pow(1024, 2)).toFixed(2)}MiB`
|
|
||||||
if (bytes < Math.pow(1024, 4)) return `${(bytes / Math.pow(1024, 3)).toFixed(2)}GiB`
|
|
||||||
if (bytes < Math.pow(1024, 5)) return `${(bytes / Math.pow(1024, 4)).toFixed(2)}TiB`
|
|
||||||
return "not only is it big, it's large"
|
|
||||||
}
|
|
||||||
|
|
||||||
const API_VERSION = 1
|
|
||||||
const ENDPOINT = `/api/v${API_VERSION}`
|
|
||||||
|
|
||||||
interface InfoPayload {
|
|
||||||
count?: number
|
|
||||||
hakurei_version?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
async function infoRequest(): Promise<InfoPayload> {
|
|
||||||
const res = await fetch(`${ENDPOINT}/info`)
|
|
||||||
const payload = await res.json()
|
|
||||||
return payload as InfoPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GetPayload {
|
|
||||||
values?: PackageIndexEntry[]
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SortOrders {
|
|
||||||
DeclarationAscending,
|
|
||||||
DeclarationDescending,
|
|
||||||
NameAscending,
|
|
||||||
NameDescending
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRequest(limit: number, index: number, sort: SortOrders): Promise<GetPayload> {
|
|
||||||
const res = await fetch(`${ENDPOINT}/get?limit=${limit}&index=${index}&sort=${sort.valueOf()}`)
|
|
||||||
const payload = await res.json()
|
|
||||||
return payload as GetPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchResult extends PackageIndexEntry {
|
|
||||||
name_matches: [number, number][]
|
|
||||||
desc_matches: [number, number][]
|
|
||||||
score: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchPayload {
|
|
||||||
count?: number
|
|
||||||
values?: SearchResult[]
|
|
||||||
}
|
|
||||||
|
|
||||||
async function searchRequest(limit: number, index: number, search: string, desc: boolean): Promise<SearchPayload> {
|
|
||||||
const res = await fetch(`${ENDPOINT}/search?limit=${limit}&index=${index}&search=${encodeURIComponent(search)}&desc=${desc}`)
|
|
||||||
if (!res.ok) {
|
|
||||||
exitSearch()
|
|
||||||
alert("invalid search query!")
|
|
||||||
return Promise.reject(res.statusText)
|
|
||||||
}
|
|
||||||
const payload = await res.json()
|
|
||||||
return payload as SearchPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
class State {
|
|
||||||
entriesPerPage: number = 10
|
|
||||||
entryIndex: number = 0
|
|
||||||
maxTotal: number = 0
|
|
||||||
maxEntries: number = 0
|
|
||||||
sort: SortOrders = SortOrders.DeclarationAscending
|
|
||||||
search: boolean = false
|
|
||||||
|
|
||||||
getEntriesPerPage(): number {
|
|
||||||
return this.entriesPerPage
|
|
||||||
}
|
|
||||||
|
|
||||||
setEntriesPerPage(entriesPerPage: number) {
|
|
||||||
this.entriesPerPage = entriesPerPage
|
|
||||||
this.setEntryIndex(Math.floor(this.getEntryIndex() / entriesPerPage) * entriesPerPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
getEntryIndex(): number {
|
|
||||||
return this.entryIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
setEntryIndex(entryIndex: number) {
|
|
||||||
this.entryIndex = entryIndex
|
|
||||||
this.updatePage()
|
|
||||||
this.updateRange()
|
|
||||||
this.updateListings()
|
|
||||||
}
|
|
||||||
|
|
||||||
getMaxTotal(): number {
|
|
||||||
return this.maxTotal
|
|
||||||
}
|
|
||||||
|
|
||||||
setMaxTotal(max: number) {
|
|
||||||
this.maxTotal = max
|
|
||||||
}
|
|
||||||
|
|
||||||
getSortOrder(): SortOrders {
|
|
||||||
return this.sort
|
|
||||||
}
|
|
||||||
|
|
||||||
setSortOrder(sortOrder: SortOrders) {
|
|
||||||
this.sort = sortOrder
|
|
||||||
this.setEntryIndex(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePage() {
|
|
||||||
let page = Math.ceil(((this.getEntryIndex() + this.getEntriesPerPage()) - 1) / this.getEntriesPerPage())
|
|
||||||
for (let e of document.getElementsByClassName("page-number")) {
|
|
||||||
(e as HTMLInputElement).value = String(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateRange() {
|
|
||||||
let max = Math.min(this.getEntryIndex() + this.getEntriesPerPage(), this.getMaxTotal())
|
|
||||||
document.getElementById("entry-counter")!.textContent = `${this.getEntryIndex() + 1}-${max} of ${this.getMaxTotal()}`
|
|
||||||
if (this.search) {
|
|
||||||
document.getElementById("search-entry-counter")!.textContent = `${this.getEntryIndex() + 1}-${max} of ${this.maxTotal}/${this.maxEntries}`
|
|
||||||
document.getElementById("search-query")!.innerHTML = `<code>${escapeHtml(this.getSearchQuery())}</code>`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getSearchQuery(): string {
|
|
||||||
let queryString = document.getElementById("search")!;
|
|
||||||
return (queryString as HTMLInputElement).value
|
|
||||||
}
|
|
||||||
|
|
||||||
getIncludeDescriptions(): boolean {
|
|
||||||
let includeDesc = document.getElementById("include-desc")!;
|
|
||||||
return (includeDesc as HTMLInputElement).checked
|
|
||||||
}
|
|
||||||
|
|
||||||
updateListings() {
|
|
||||||
if (this.search) {
|
|
||||||
searchRequest(this.getEntriesPerPage(), this.getEntryIndex(), this.getSearchQuery(), this.getIncludeDescriptions())
|
|
||||||
.then(res => {
|
|
||||||
let table = document.getElementById("pkg-list")!
|
|
||||||
table.innerHTML = ''
|
|
||||||
for (let row of res.values!) {
|
|
||||||
table.appendChild(entryToHTML(row))
|
|
||||||
}
|
|
||||||
STATE.maxTotal = res.count!
|
|
||||||
STATE.updateRange()
|
|
||||||
if(res.count! < 1) {
|
|
||||||
exitSearch()
|
|
||||||
alert("no results found!")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
getRequest(this.getEntriesPerPage(), this.getEntryIndex(), this.getSortOrder())
|
|
||||||
.then(res => {
|
|
||||||
let table = document.getElementById("pkg-list")!
|
|
||||||
table.innerHTML = ''
|
|
||||||
for (let row of res.values!) {
|
|
||||||
table.appendChild(entryToHTML(row))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let STATE: State
|
|
||||||
|
|
||||||
|
|
||||||
function lastPageIndex(): number {
|
|
||||||
return Math.floor(STATE.getMaxTotal() / STATE.getEntriesPerPage()) * STATE.getEntriesPerPage()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPage(page: number) {
|
|
||||||
STATE.setEntryIndex(Math.max(0, Math.min(STATE.getEntriesPerPage() * (page - 1), lastPageIndex())))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function escapeHtml(str?: string): string {
|
|
||||||
let out: string = ''
|
|
||||||
if (str == undefined) return ""
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
out += escapeHtmlChar(str[i])
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtmlChar(char: string): string {
|
|
||||||
if (char.length != 1) return char
|
|
||||||
switch (char[0]) {
|
|
||||||
case '&':
|
|
||||||
return "&"
|
|
||||||
case '<':
|
|
||||||
return "<"
|
|
||||||
case '>':
|
|
||||||
return ">"
|
|
||||||
case '"':
|
|
||||||
return """
|
|
||||||
case "'":
|
|
||||||
return "'"
|
|
||||||
default:
|
|
||||||
return char
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function firstPage() {
|
|
||||||
STATE.setEntryIndex(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function prevPage() {
|
|
||||||
let index = STATE.getEntryIndex()
|
|
||||||
STATE.setEntryIndex(Math.max(0, index - STATE.getEntriesPerPage()))
|
|
||||||
}
|
|
||||||
|
|
||||||
function lastPage() {
|
|
||||||
STATE.setEntryIndex(lastPageIndex())
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextPage() {
|
|
||||||
let index = STATE.getEntryIndex()
|
|
||||||
STATE.setEntryIndex(Math.min(lastPageIndex(), index + STATE.getEntriesPerPage()))
|
|
||||||
}
|
|
||||||
|
|
||||||
function doSearch() {
|
|
||||||
document.getElementById("top-controls-regular")!.toggleAttribute("hidden");
|
|
||||||
document.getElementById("search-top-controls")!.toggleAttribute("hidden");
|
|
||||||
STATE.search = true;
|
|
||||||
STATE.setEntryIndex(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function exitSearch() {
|
|
||||||
document.getElementById("top-controls-regular")!.toggleAttribute("hidden");
|
|
||||||
document.getElementById("search-top-controls")!.toggleAttribute("hidden");
|
|
||||||
STATE.search = false;
|
|
||||||
STATE.setMaxTotal(STATE.maxEntries)
|
|
||||||
STATE.setEntryIndex(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
STATE = new State()
|
|
||||||
infoRequest()
|
|
||||||
.then(res => {
|
|
||||||
STATE.maxEntries = res.count!
|
|
||||||
STATE.setMaxTotal(STATE.maxEntries)
|
|
||||||
document.getElementById("hakurei-version")!.textContent = res.hakurei_version!
|
|
||||||
STATE.updateRange()
|
|
||||||
STATE.updateListings()
|
|
||||||
})
|
|
||||||
for (let e of document.getElementsByClassName("page-number")) {
|
|
||||||
e.addEventListener("change", (_) => {
|
|
||||||
setPage(parseInt((e as HTMLInputElement).value))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
document.getElementById("count")?.addEventListener("change", (event) => {
|
|
||||||
STATE.setEntriesPerPage(parseInt((event.target as HTMLSelectElement).value))
|
|
||||||
})
|
|
||||||
document.getElementById("sort")?.addEventListener("change", (event) => {
|
|
||||||
STATE.setSortOrder(parseInt((event.target as HTMLSelectElement).value))
|
|
||||||
})
|
|
||||||
document.getElementById("search")?.addEventListener("keyup", (event) => {
|
|
||||||
if (event.key === 'Enter') doSearch()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
.page-number {
|
|
||||||
width: 2em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.page-number {
|
|
||||||
width: 2em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
html {
|
|
||||||
background-color: #2c2c2c;
|
|
||||||
color: ghostwhite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
html {
|
|
||||||
background-color: #d3d3d3;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2024",
|
|
||||||
"strict": true,
|
|
||||||
"alwaysStrict": true,
|
|
||||||
"outDir": "static"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
// Package ui holds the static web UI.
|
|
||||||
package ui
|
|
||||||
|
|
||||||
import "net/http"
|
|
||||||
|
|
||||||
// Register arranges for mux to serve the embedded frontend.
|
|
||||||
func Register(mux *http.ServeMux) {
|
|
||||||
mux.Handle("GET /", http.FileServer(http.FS(static)))
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
//go:build frontend
|
|
||||||
|
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"io/fs"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:generate tsc
|
|
||||||
//go:generate cp index.html style.css static
|
|
||||||
//go:embed static
|
|
||||||
var _static embed.FS
|
|
||||||
|
|
||||||
var static = func() fs.FS {
|
|
||||||
if f, err := fs.Sub(_static, "static"); err != nil {
|
|
||||||
panic(err)
|
|
||||||
} else {
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
//go:build !frontend
|
|
||||||
|
|
||||||
package ui
|
|
||||||
|
|
||||||
import "testing/fstest"
|
|
||||||
|
|
||||||
var static fstest.MapFS
|
|
||||||
-830
@@ -1,830 +0,0 @@
|
|||||||
// The mbf program is a frontend for [hakurei.app/internal/rosa].
|
|
||||||
//
|
|
||||||
// This program is not covered by the compatibility promise. The command line
|
|
||||||
// interface, available packages and their behaviour, and even the on-disk
|
|
||||||
// format, may change at any time.
|
|
||||||
//
|
|
||||||
// # Name
|
|
||||||
//
|
|
||||||
// The name mbf stands for maiden's best friend, as a tribute to the DOOM source
|
|
||||||
// port of [the same name]. This name is a placeholder and is subject to change.
|
|
||||||
//
|
|
||||||
// [the same name]: https://www.doomwiki.org/wiki/MBF
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/sha512"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
"unique"
|
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
"hakurei.app/command"
|
|
||||||
"hakurei.app/container"
|
|
||||||
"hakurei.app/container/seccomp"
|
|
||||||
"hakurei.app/container/std"
|
|
||||||
"hakurei.app/ext"
|
|
||||||
"hakurei.app/fhs"
|
|
||||||
"hakurei.app/internal/pkg"
|
|
||||||
"hakurei.app/internal/rosa"
|
|
||||||
"hakurei.app/message"
|
|
||||||
|
|
||||||
"hakurei.app/cmd/mbf/internal/pkgserver"
|
|
||||||
"hakurei.app/cmd/mbf/internal/pkgserver/ui"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
container.TryArgv0(nil)
|
|
||||||
|
|
||||||
log.SetFlags(0)
|
|
||||||
log.SetPrefix("mbf: ")
|
|
||||||
msg := message.New(log.Default())
|
|
||||||
|
|
||||||
if os.Geteuid() == 0 {
|
|
||||||
log.Fatal("this program must not run as root")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(),
|
|
||||||
syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
|
||||||
defer stop()
|
|
||||||
|
|
||||||
var cm cache
|
|
||||||
defer func() { cm.Close() }()
|
|
||||||
|
|
||||||
var (
|
|
||||||
flagQuiet bool
|
|
||||||
flagQEMU bool
|
|
||||||
flagArch string
|
|
||||||
flagCheck bool
|
|
||||||
flagLTO bool
|
|
||||||
|
|
||||||
flagCrossOverride int
|
|
||||||
|
|
||||||
addr net.UnixAddr
|
|
||||||
)
|
|
||||||
c := command.New(os.Stderr, log.Printf, "mbf", func([]string) error {
|
|
||||||
msg.SwapVerbose(!flagQuiet)
|
|
||||||
cm.ctx, cm.msg = ctx, msg
|
|
||||||
cm.base = os.ExpandEnv(cm.base)
|
|
||||||
if cm.base == "" {
|
|
||||||
cm.base = "cache"
|
|
||||||
}
|
|
||||||
|
|
||||||
addr.Net = "unix"
|
|
||||||
addr.Name = os.ExpandEnv(addr.Name)
|
|
||||||
if addr.Name == "" {
|
|
||||||
addr.Name = filepath.Join(cm.base, "daemon")
|
|
||||||
}
|
|
||||||
|
|
||||||
var flags int
|
|
||||||
if !flagCheck {
|
|
||||||
flags |= rosa.OptSkipCheck
|
|
||||||
}
|
|
||||||
if !flagLTO {
|
|
||||||
flags |= rosa.OptLLVMNoLTO
|
|
||||||
}
|
|
||||||
rosa.DropCaches("", flags)
|
|
||||||
cross := flagArch != "" && flagArch != runtime.GOARCH
|
|
||||||
if flagQEMU || cross {
|
|
||||||
cm.qemu = rosa.Std.Load(rosa.QEMU)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cross {
|
|
||||||
if flagCrossOverride != -1 {
|
|
||||||
flags = flagCrossOverride
|
|
||||||
}
|
|
||||||
|
|
||||||
rosa.DropCaches(flagArch, flags)
|
|
||||||
if !rosa.HasStage0() {
|
|
||||||
return pkg.UnsupportedArchError(flagArch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}).Flag(
|
|
||||||
&flagQuiet,
|
|
||||||
"q", command.BoolFlag(false),
|
|
||||||
"Do not print cure messages",
|
|
||||||
).Flag(
|
|
||||||
&flagQEMU,
|
|
||||||
"register", command.BoolFlag(false),
|
|
||||||
"Enable additional target architectures",
|
|
||||||
).Flag(
|
|
||||||
&flagArch,
|
|
||||||
"arch", command.StringFlag(runtime.GOARCH),
|
|
||||||
"Target architecture",
|
|
||||||
).Flag(
|
|
||||||
&flagLTO,
|
|
||||||
"lto", command.BoolFlag(false),
|
|
||||||
"Enable LTO in stage2 and stage3 LLVM toolchains",
|
|
||||||
).Flag(
|
|
||||||
&flagCheck,
|
|
||||||
"check", command.BoolFlag(true),
|
|
||||||
"Run test suites",
|
|
||||||
).Flag(
|
|
||||||
&flagCrossOverride,
|
|
||||||
"cross-flags", command.IntFlag(-1),
|
|
||||||
"Override non-native target preset flags",
|
|
||||||
).Flag(
|
|
||||||
&cm.verboseInit,
|
|
||||||
"v", command.BoolFlag(false),
|
|
||||||
"Do not suppress verbose output from init",
|
|
||||||
).Flag(
|
|
||||||
&cm.cures,
|
|
||||||
"cures", command.IntFlag(0),
|
|
||||||
"Maximum number of dependencies to cure at any given time",
|
|
||||||
).Flag(
|
|
||||||
&cm.jobs,
|
|
||||||
"jobs", command.IntFlag(0),
|
|
||||||
"Preferred number of jobs to run, when applicable",
|
|
||||||
).Flag(
|
|
||||||
&cm.base,
|
|
||||||
"d", command.StringFlag("$MBF_CACHE_DIR"),
|
|
||||||
"Directory to store cured artifacts",
|
|
||||||
).Flag(
|
|
||||||
&cm.idle,
|
|
||||||
"sched-idle", command.BoolFlag(false),
|
|
||||||
"Set SCHED_IDLE scheduling policy",
|
|
||||||
).Flag(
|
|
||||||
&cm.hostAbstract,
|
|
||||||
"host-abstract", command.BoolFlag(
|
|
||||||
os.Getenv("MBF_HOST_ABSTRACT") != "",
|
|
||||||
),
|
|
||||||
"Do not restrict networked cure containers from connecting to host "+
|
|
||||||
"abstract UNIX sockets",
|
|
||||||
).Flag(
|
|
||||||
&addr.Name,
|
|
||||||
"socket", command.StringFlag("$MBF_DAEMON_SOCKET"),
|
|
||||||
"Pathname of socket to bind to",
|
|
||||||
)
|
|
||||||
|
|
||||||
c.NewCommand(
|
|
||||||
"checksum", "Compute checksum of data read from standard input",
|
|
||||||
func([]string) error {
|
|
||||||
done := make(chan struct{})
|
|
||||||
defer close(done)
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
os.Exit(1)
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
h := sha512.New384()
|
|
||||||
if _, err := io.Copy(h, os.Stdin); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Println(pkg.Encode(pkg.Checksum(h.Sum(nil))))
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
{
|
|
||||||
var flagShifts int
|
|
||||||
c.NewCommand(
|
|
||||||
"scrub", "Examine the on-disk cache for errors",
|
|
||||||
func(args []string) error {
|
|
||||||
if len(args) > 0 {
|
|
||||||
return errors.New("scrub expects no arguments")
|
|
||||||
}
|
|
||||||
if flagShifts < 0 || flagShifts > 31 {
|
|
||||||
flagShifts = 12
|
|
||||||
}
|
|
||||||
return cm.Do(func(cache *pkg.Cache) error {
|
|
||||||
return cache.Scrub(runtime.NumCPU() << flagShifts)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
).Flag(
|
|
||||||
&flagShifts,
|
|
||||||
"shift", command.IntFlag(12),
|
|
||||||
"Scrub parallelism size exponent, to the power of 2",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
var (
|
|
||||||
flagBind string
|
|
||||||
flagStatus bool
|
|
||||||
flagReport string
|
|
||||||
)
|
|
||||||
c.NewCommand(
|
|
||||||
"info",
|
|
||||||
"Display out-of-band metadata of an artifact",
|
|
||||||
func(args []string) (err error) {
|
|
||||||
const shutdownTimeout = 15 * time.Second
|
|
||||||
|
|
||||||
var r *rosa.Report
|
|
||||||
if flagReport != "" {
|
|
||||||
if r, err = rosa.OpenReport(flagReport); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if closeErr := r.Close(); err == nil {
|
|
||||||
err = closeErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
defer r.HandleAccess(&err)()
|
|
||||||
}
|
|
||||||
|
|
||||||
if flagBind == "" {
|
|
||||||
return commandInfo(&cm, args, os.Stdout, flagStatus, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
var mux http.ServeMux
|
|
||||||
ui.Register(&mux)
|
|
||||||
if err = pkgserver.Register(ctx, &mux, r); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
server := http.Server{Addr: flagBind, Handler: &mux}
|
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
|
||||||
cc, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
|
||||||
defer cancel()
|
|
||||||
if _err := server.Shutdown(cc); _err != nil {
|
|
||||||
log.Fatal(_err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
msg.Verbosef("listening on %q", flagBind)
|
|
||||||
err = server.ListenAndServe()
|
|
||||||
if errors.Is(err, http.ErrServerClosed) {
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
return
|
|
||||||
},
|
|
||||||
).Flag(
|
|
||||||
&flagBind,
|
|
||||||
"bind", command.StringFlag(""),
|
|
||||||
"TCP address for the server to listen on",
|
|
||||||
).Flag(
|
|
||||||
&flagStatus,
|
|
||||||
"status", command.BoolFlag(false),
|
|
||||||
"Display cure status if available",
|
|
||||||
).Flag(
|
|
||||||
&flagReport,
|
|
||||||
"report", command.StringFlag(""),
|
|
||||||
"Load cure status from this report file instead of cache",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.NewCommand(
|
|
||||||
"report",
|
|
||||||
"Generate an artifact cure report for the current cache",
|
|
||||||
func(args []string) (err error) {
|
|
||||||
var w *os.File
|
|
||||||
switch len(args) {
|
|
||||||
case 0:
|
|
||||||
w = os.Stdout
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
if w, err = os.OpenFile(
|
|
||||||
args[0],
|
|
||||||
os.O_CREATE|os.O_EXCL|syscall.O_WRONLY,
|
|
||||||
0400,
|
|
||||||
); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
closeErr := w.Close()
|
|
||||||
if err == nil {
|
|
||||||
err = closeErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
default:
|
|
||||||
return errors.New("report requires 1 argument")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ext.Isatty(int(w.Fd())) {
|
|
||||||
return errors.New("output appears to be a terminal")
|
|
||||||
}
|
|
||||||
return cm.Do(func(cache *pkg.Cache) error {
|
|
||||||
return rosa.WriteReport(msg, w, cache)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
{
|
|
||||||
var flagJobs int
|
|
||||||
c.NewCommand("updates", command.UsageInternal, func([]string) error {
|
|
||||||
var (
|
|
||||||
errsMu sync.Mutex
|
|
||||||
errs []error
|
|
||||||
|
|
||||||
n atomic.Uint64
|
|
||||||
)
|
|
||||||
|
|
||||||
w := make(chan rosa.PArtifact)
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
for range max(flagJobs, 1) {
|
|
||||||
wg.Go(func() {
|
|
||||||
for p := range w {
|
|
||||||
meta := rosa.GetMetadata(p)
|
|
||||||
if meta.ID == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
v, err := meta.GetVersions(ctx)
|
|
||||||
if err != nil {
|
|
||||||
errsMu.Lock()
|
|
||||||
errs = append(errs, err)
|
|
||||||
errsMu.Unlock()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if current, latest :=
|
|
||||||
rosa.Std.Version(p),
|
|
||||||
meta.GetLatest(v); current != latest {
|
|
||||||
|
|
||||||
n.Add(1)
|
|
||||||
log.Printf("%s %s < %s", meta.Name, current, latest)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
msg.Verbosef("%s is up to date", meta.Name)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
done:
|
|
||||||
for i := range rosa.PresetEnd {
|
|
||||||
select {
|
|
||||||
case w <- rosa.PArtifact(i):
|
|
||||||
break
|
|
||||||
case <-ctx.Done():
|
|
||||||
break done
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(w)
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if v := n.Load(); v > 0 {
|
|
||||||
errs = append(errs, errors.New(strconv.Itoa(int(v))+
|
|
||||||
" package(s) are out of date"))
|
|
||||||
}
|
|
||||||
return errors.Join(errs...)
|
|
||||||
}).Flag(
|
|
||||||
&flagJobs,
|
|
||||||
"j", command.IntFlag(32),
|
|
||||||
"Maximum number of simultaneous connections",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.NewCommand(
|
|
||||||
"daemon",
|
|
||||||
"Service artifact IR with Rosa OS extensions",
|
|
||||||
func(args []string) error {
|
|
||||||
ul, err := net.ListenUnix("unix", &addr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Printf("listening on pathname socket at %s", addr.Name)
|
|
||||||
return serve(ctx, log.Default(), &cm, ul)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
{
|
|
||||||
var (
|
|
||||||
flagGentoo string
|
|
||||||
flagChecksum string
|
|
||||||
|
|
||||||
flagStage0 bool
|
|
||||||
)
|
|
||||||
c.NewCommand(
|
|
||||||
"stage3",
|
|
||||||
"Check for toolchain 3-stage non-determinism",
|
|
||||||
func(args []string) (err error) {
|
|
||||||
t := rosa.Std
|
|
||||||
if flagGentoo != "" {
|
|
||||||
t -= 3 // magic number to discourage misuse
|
|
||||||
|
|
||||||
var checksum pkg.Checksum
|
|
||||||
if len(flagChecksum) != 0 {
|
|
||||||
if err = pkg.Decode(&checksum, flagChecksum); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rosa.SetGentooStage3(flagGentoo, checksum)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
pathname *check.Absolute
|
|
||||||
checksum [2]unique.Handle[pkg.Checksum]
|
|
||||||
)
|
|
||||||
|
|
||||||
if err = cm.Do(func(cache *pkg.Cache) (err error) {
|
|
||||||
pathname, _, err = cache.Cure(
|
|
||||||
(t - 2).Load(rosa.LLVM),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Println("stage1:", pathname)
|
|
||||||
|
|
||||||
if err = cm.Do(func(cache *pkg.Cache) (err error) {
|
|
||||||
pathname, checksum[0], err = cache.Cure(
|
|
||||||
(t - 1).Load(rosa.LLVM),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Println("stage2:", pathname)
|
|
||||||
if err = cm.Do(func(cache *pkg.Cache) (err error) {
|
|
||||||
pathname, checksum[1], err = cache.Cure(
|
|
||||||
t.Load(rosa.LLVM),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Println("stage3:", pathname)
|
|
||||||
|
|
||||||
if checksum[0] != checksum[1] {
|
|
||||||
err = &pkg.ChecksumMismatchError{
|
|
||||||
Got: checksum[0].Value(),
|
|
||||||
Want: checksum[1].Value(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Println(
|
|
||||||
"stage2 is identical to stage3",
|
|
||||||
"("+pkg.Encode(checksum[0].Value())+")",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if flagStage0 {
|
|
||||||
if err = cm.Do(func(cache *pkg.Cache) (err error) {
|
|
||||||
pathname, _, err = cache.Cure(
|
|
||||||
t.Load(rosa.Stage0),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Println(pathname)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
},
|
|
||||||
).Flag(
|
|
||||||
&flagGentoo,
|
|
||||||
"gentoo", command.StringFlag(""),
|
|
||||||
"Bootstrap from a Gentoo stage3 tarball",
|
|
||||||
).Flag(
|
|
||||||
&flagChecksum,
|
|
||||||
"checksum", command.StringFlag(""),
|
|
||||||
"Checksum of Gentoo stage3 tarball",
|
|
||||||
).Flag(
|
|
||||||
&flagStage0,
|
|
||||||
"stage0", command.BoolFlag(false),
|
|
||||||
"Create bootstrap stage0 tarball",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
var (
|
|
||||||
flagDump string
|
|
||||||
flagEnter bool
|
|
||||||
flagExport string
|
|
||||||
flagRemote bool
|
|
||||||
flagNoReply bool
|
|
||||||
|
|
||||||
flagBoot bool
|
|
||||||
flagStd bool
|
|
||||||
)
|
|
||||||
c.NewCommand(
|
|
||||||
"cure",
|
|
||||||
"Cure the named artifact and show its path",
|
|
||||||
func(args []string) error {
|
|
||||||
if len(args) != 1 {
|
|
||||||
return errors.New("cure requires 1 argument")
|
|
||||||
}
|
|
||||||
p, ok := rosa.ResolveName(args[0])
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("unknown artifact %q", args[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
t := rosa.Std
|
|
||||||
if flagBoot {
|
|
||||||
t -= 2
|
|
||||||
} else if flagStd {
|
|
||||||
t -= 1
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
default:
|
|
||||||
var pathname *check.Absolute
|
|
||||||
err := cm.Do(func(cache *pkg.Cache) (err error) {
|
|
||||||
pathname, _, err = cache.Cure(t.Load(p))
|
|
||||||
return
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Println(pathname)
|
|
||||||
|
|
||||||
if flagExport != "" {
|
|
||||||
msg.Verbosef("exporting %s to %s...", args[0], flagExport)
|
|
||||||
|
|
||||||
var f *os.File
|
|
||||||
if f, err = os.OpenFile(
|
|
||||||
flagExport,
|
|
||||||
os.O_WRONLY|os.O_CREATE|os.O_EXCL,
|
|
||||||
0400,
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
} else if _, err = pkg.Flatten(
|
|
||||||
os.DirFS(pathname.String()),
|
|
||||||
".",
|
|
||||||
f,
|
|
||||||
); err != nil {
|
|
||||||
_ = f.Close()
|
|
||||||
return err
|
|
||||||
} else if err = f.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
|
|
||||||
case flagDump != "":
|
|
||||||
f, err := os.OpenFile(
|
|
||||||
flagDump,
|
|
||||||
os.O_WRONLY|os.O_CREATE|os.O_EXCL,
|
|
||||||
0644,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = pkg.NewIR().EncodeAll(f, rosa.Std.Load(p)); err != nil {
|
|
||||||
_ = f.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return f.Close()
|
|
||||||
|
|
||||||
case flagEnter:
|
|
||||||
return cm.Do(func(cache *pkg.Cache) error {
|
|
||||||
return cache.EnterExec(
|
|
||||||
ctx,
|
|
||||||
t.Load(p),
|
|
||||||
true, os.Stdin, os.Stdout, os.Stderr,
|
|
||||||
rosa.AbsSystem.Append("bin", "mksh"),
|
|
||||||
"sh",
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
case flagRemote:
|
|
||||||
var flags uint64
|
|
||||||
if flagNoReply {
|
|
||||||
flags |= remoteNoReply
|
|
||||||
}
|
|
||||||
a := t.Load(p)
|
|
||||||
pathname, err := cureRemote(ctx, &addr, a, flags)
|
|
||||||
if !flagNoReply && err == nil {
|
|
||||||
log.Println(pathname)
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.Is(err, context.Canceled) {
|
|
||||||
cc, cancel := context.WithDeadline(context.Background(), daemonDeadline())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if _err := cancelRemote(cc, &addr, a, false); _err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
},
|
|
||||||
).Flag(
|
|
||||||
&flagDump,
|
|
||||||
"dump", command.StringFlag(""),
|
|
||||||
"Write IR to specified pathname and terminate",
|
|
||||||
).Flag(
|
|
||||||
&flagExport,
|
|
||||||
"export", command.StringFlag(""),
|
|
||||||
"Export cured artifact to specified pathname",
|
|
||||||
).Flag(
|
|
||||||
&flagEnter,
|
|
||||||
"enter", command.BoolFlag(false),
|
|
||||||
"Enter cure container with an interactive shell",
|
|
||||||
).Flag(
|
|
||||||
&flagRemote,
|
|
||||||
"daemon", command.BoolFlag(false),
|
|
||||||
"Cure artifact on the daemon",
|
|
||||||
).Flag(
|
|
||||||
&flagNoReply,
|
|
||||||
"no-reply", command.BoolFlag(false),
|
|
||||||
"Do not receive a reply from the daemon",
|
|
||||||
).Flag(
|
|
||||||
&flagBoot,
|
|
||||||
"boot", command.BoolFlag(false),
|
|
||||||
"Build on the stage0 toolchain",
|
|
||||||
).Flag(
|
|
||||||
&flagStd,
|
|
||||||
"std", command.BoolFlag(false),
|
|
||||||
"Build on the intermediate toolchain",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.NewCommand(
|
|
||||||
"abort",
|
|
||||||
"Abort all pending cures on the daemon",
|
|
||||||
func([]string) error { return abortRemote(ctx, &addr, false) },
|
|
||||||
)
|
|
||||||
|
|
||||||
{
|
|
||||||
var (
|
|
||||||
flagNet bool
|
|
||||||
flagSession bool
|
|
||||||
|
|
||||||
flagWithToolchain bool
|
|
||||||
)
|
|
||||||
c.NewCommand(
|
|
||||||
"shell",
|
|
||||||
"Interactive shell in the specified Rosa OS environment",
|
|
||||||
func(args []string) error {
|
|
||||||
presets := make([]rosa.PArtifact, len(args)+3)
|
|
||||||
for i, arg := range args {
|
|
||||||
p, ok := rosa.ResolveName(arg)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("unknown artifact %q", arg)
|
|
||||||
}
|
|
||||||
presets[i] = p
|
|
||||||
}
|
|
||||||
|
|
||||||
base := rosa.LLVM
|
|
||||||
if !flagWithToolchain {
|
|
||||||
base = rosa.Musl
|
|
||||||
}
|
|
||||||
presets = append(presets,
|
|
||||||
base,
|
|
||||||
rosa.Mksh,
|
|
||||||
rosa.Toybox,
|
|
||||||
)
|
|
||||||
|
|
||||||
root := make(pkg.Collect, 0, 6+len(args))
|
|
||||||
root = rosa.Std.AppendPresets(root, presets...)
|
|
||||||
|
|
||||||
if err := cm.Do(func(cache *pkg.Cache) error {
|
|
||||||
_, _, err := cache.Cure(&root)
|
|
||||||
return err
|
|
||||||
}); err == nil {
|
|
||||||
return errors.New("unreachable")
|
|
||||||
} else if !pkg.IsCollected(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
type cureRes struct {
|
|
||||||
pathname *check.Absolute
|
|
||||||
checksum unique.Handle[pkg.Checksum]
|
|
||||||
}
|
|
||||||
cured := make(map[pkg.Artifact]cureRes)
|
|
||||||
for _, a := range root {
|
|
||||||
if err := cm.Do(func(cache *pkg.Cache) error {
|
|
||||||
pathname, checksum, err := cache.Cure(a)
|
|
||||||
if err == nil {
|
|
||||||
cured[a] = cureRes{pathname, checksum}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// explicitly open for direct error-free use from this point
|
|
||||||
if cm.c == nil {
|
|
||||||
if err := cm.open(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layers := pkg.PromoteLayers(root, func(a pkg.Artifact) (
|
|
||||||
*check.Absolute,
|
|
||||||
unique.Handle[pkg.Checksum],
|
|
||||||
) {
|
|
||||||
res := cured[a]
|
|
||||||
return res.pathname, res.checksum
|
|
||||||
}, func(i int, d pkg.Artifact) {
|
|
||||||
r := pkg.Encode(cm.c.Ident(d).Value())
|
|
||||||
if s, ok := d.(fmt.Stringer); ok {
|
|
||||||
if name := s.String(); name != "" {
|
|
||||||
r += "-" + name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
msg.Verbosef("promoted layer %d as %s", i, r)
|
|
||||||
})
|
|
||||||
|
|
||||||
z := container.New(ctx, msg)
|
|
||||||
z.WaitDelay = 3 * time.Second
|
|
||||||
z.SeccompPresets = pkg.SeccompPresets
|
|
||||||
z.SeccompFlags |= seccomp.AllowMultiarch
|
|
||||||
z.ParentPerm = 0700
|
|
||||||
z.HostNet = flagNet
|
|
||||||
z.RetainSession = flagSession
|
|
||||||
z.Hostname = "localhost"
|
|
||||||
z.Uid, z.Gid = (1<<10)-1, (1<<10)-1
|
|
||||||
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
|
|
||||||
z.Quiet = !cm.verboseInit
|
|
||||||
if s, ok := os.LookupEnv("TERM"); ok {
|
|
||||||
z.Env = append(z.Env, "TERM="+s)
|
|
||||||
}
|
|
||||||
|
|
||||||
var tempdir *check.Absolute
|
|
||||||
if s, err := filepath.Abs(os.TempDir()); err != nil {
|
|
||||||
return err
|
|
||||||
} else if tempdir, err = check.NewAbs(s); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
z.Dir = fhs.AbsRoot
|
|
||||||
z.Env = []string{
|
|
||||||
"SHELL=/system/bin/mksh",
|
|
||||||
"PATH=/system/bin",
|
|
||||||
"HOME=/",
|
|
||||||
}
|
|
||||||
z.Path = rosa.AbsSystem.Append("bin", "mksh")
|
|
||||||
z.Args = []string{"mksh"}
|
|
||||||
z.
|
|
||||||
OverlayEphemeral(fhs.AbsRoot, layers...).
|
|
||||||
Place(
|
|
||||||
fhs.AbsEtc.Append("hosts"),
|
|
||||||
[]byte("127.0.0.1 localhost\n"),
|
|
||||||
).
|
|
||||||
Place(
|
|
||||||
fhs.AbsEtc.Append("passwd"),
|
|
||||||
[]byte("media_rw:x:1023:1023::/:/system/bin/sh\n"+
|
|
||||||
"nobody:x:65534:65534::/proc/nonexistent:/system/bin/false\n"),
|
|
||||||
).
|
|
||||||
Place(
|
|
||||||
fhs.AbsEtc.Append("group"),
|
|
||||||
[]byte("media_rw:x:1023:\nnobody:x:65534:\n"),
|
|
||||||
).
|
|
||||||
Bind(tempdir, fhs.AbsTmp, std.BindWritable).
|
|
||||||
Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
|
|
||||||
|
|
||||||
if err := z.Start(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := z.Serve(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return z.Wait()
|
|
||||||
},
|
|
||||||
).Flag(
|
|
||||||
&flagNet,
|
|
||||||
"net", command.BoolFlag(false),
|
|
||||||
"Share host net namespace",
|
|
||||||
).Flag(
|
|
||||||
&flagSession,
|
|
||||||
"session", command.BoolFlag(true),
|
|
||||||
"Retain session",
|
|
||||||
).Flag(
|
|
||||||
&flagWithToolchain,
|
|
||||||
"with-toolchain", command.BoolFlag(false),
|
|
||||||
"Include the stage2 LLVM toolchain",
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Command(
|
|
||||||
"help",
|
|
||||||
"Show this help message",
|
|
||||||
func([]string) error { c.PrintHelp(); return nil },
|
|
||||||
)
|
|
||||||
|
|
||||||
c.MustParse(os.Args[1:], func(err error) {
|
|
||||||
cm.Close()
|
|
||||||
if w, ok := err.(interface{ Unwrap() []error }); !ok {
|
|
||||||
log.Fatal(err)
|
|
||||||
} else {
|
|
||||||
errs := w.Unwrap()
|
|
||||||
for i, e := range errs {
|
|
||||||
if i == len(errs)-1 {
|
|
||||||
log.Fatal(e)
|
|
||||||
}
|
|
||||||
log.Println(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"hakurei.app/internal/rosa"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
rosa.DropCaches("", rosa.OptLLVMNoLTO)
|
|
||||||
os.Exit(m.Run())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCureAll(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
const env = "ROSA_TEST_DAEMON"
|
|
||||||
|
|
||||||
if !testing.Verbose() {
|
|
||||||
t.Skip("verbose flag not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
pathname, ok := os.LookupEnv(env)
|
|
||||||
if !ok {
|
|
||||||
t.Skip(env + " not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
addr := net.UnixAddr{Net: "unix", Name: pathname}
|
|
||||||
t.Cleanup(func() {
|
|
||||||
if t.Failed() {
|
|
||||||
if err := abortRemote(t.Context(), &addr, false); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
for i := range rosa.PresetEnd {
|
|
||||||
p := rosa.PArtifact(i)
|
|
||||||
t.Run(rosa.GetMetadata(p).Name, func(t *testing.T) {
|
|
||||||
_, err := cureRemote(t.Context(), &addr, rosa.Std.Load(p), 0)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,282 +0,0 @@
|
|||||||
#ifndef _GNU_SOURCE
|
|
||||||
#define _GNU_SOURCE /* O_DIRECT */
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <dirent.h>
|
|
||||||
#include <errno.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
/* TODO(ophestra): remove after 05ce67fea99ca09cd4b6625cff7aec9cc222dd5a reaches a release */
|
|
||||||
#include <sys/syscall.h>
|
|
||||||
|
|
||||||
#include "fuse-operations.h"
|
|
||||||
|
|
||||||
/* MUST_TRANSLATE_PATHNAME translates a userspace pathname to a relative pathname;
|
|
||||||
* the resulting address points to a constant string or part of pathname, it is never heap allocated. */
|
|
||||||
#define MUST_TRANSLATE_PATHNAME(pathname) \
|
|
||||||
do { \
|
|
||||||
if (pathname == NULL) \
|
|
||||||
return -EINVAL; \
|
|
||||||
while (*pathname == '/') \
|
|
||||||
pathname++; \
|
|
||||||
if (*pathname == '\0') \
|
|
||||||
pathname = "."; \
|
|
||||||
} while (0)
|
|
||||||
|
|
||||||
/* GET_CONTEXT_PRIV obtains fuse context and private data for the calling thread. */
|
|
||||||
#define GET_CONTEXT_PRIV(ctx, priv) \
|
|
||||||
do { \
|
|
||||||
ctx = fuse_get_context(); \
|
|
||||||
priv = ctx->private_data; \
|
|
||||||
} while (0)
|
|
||||||
|
|
||||||
/* impl_getattr modifies a struct stat from the kernel to present to userspace;
|
|
||||||
* impl_getattr returns a negative errno style error code. */
|
|
||||||
static int impl_getattr(struct fuse_context *ctx, struct stat *statbuf) {
|
|
||||||
/* allowlist of permitted types */
|
|
||||||
if (!S_ISDIR(statbuf->st_mode) && !S_ISREG(statbuf->st_mode) && !S_ISLNK(statbuf->st_mode)) {
|
|
||||||
return -ENOTRECOVERABLE; /* returning an errno causes all operations on the file to return EIO */
|
|
||||||
}
|
|
||||||
|
|
||||||
#define OVERRIDE_PERM(v) (statbuf->st_mode & ~0777) | (v & 0777)
|
|
||||||
if (S_ISDIR(statbuf->st_mode))
|
|
||||||
statbuf->st_mode = OVERRIDE_PERM(SHAREFS_PERM_DIR);
|
|
||||||
else if (S_ISREG(statbuf->st_mode))
|
|
||||||
statbuf->st_mode = OVERRIDE_PERM(SHAREFS_PERM_REG);
|
|
||||||
else
|
|
||||||
statbuf->st_mode = 0; /* should always be symlink in this case */
|
|
||||||
|
|
||||||
statbuf->st_uid = ctx->uid;
|
|
||||||
statbuf->st_gid = SHAREFS_MEDIA_RW_ID;
|
|
||||||
statbuf->st_ctim = statbuf->st_mtim;
|
|
||||||
statbuf->st_nlink = 1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* fuse_operations implementation */
|
|
||||||
|
|
||||||
int sharefs_getattr(const char *pathname, struct stat *statbuf, struct fuse_file_info *fi) {
|
|
||||||
struct fuse_context *ctx;
|
|
||||||
struct sharefs_private *priv;
|
|
||||||
GET_CONTEXT_PRIV(ctx, priv);
|
|
||||||
MUST_TRANSLATE_PATHNAME(pathname);
|
|
||||||
|
|
||||||
(void)fi;
|
|
||||||
|
|
||||||
if (fstatat(priv->dirfd, pathname, statbuf, AT_SYMLINK_NOFOLLOW) == -1)
|
|
||||||
return -errno;
|
|
||||||
return impl_getattr(ctx, statbuf);
|
|
||||||
}
|
|
||||||
|
|
||||||
int sharefs_readdir(const char *pathname, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags) {
|
|
||||||
int fd;
|
|
||||||
DIR *dp;
|
|
||||||
struct stat st;
|
|
||||||
int ret = 0;
|
|
||||||
struct dirent *de;
|
|
||||||
|
|
||||||
struct fuse_context *ctx;
|
|
||||||
struct sharefs_private *priv;
|
|
||||||
GET_CONTEXT_PRIV(ctx, priv);
|
|
||||||
MUST_TRANSLATE_PATHNAME(pathname);
|
|
||||||
|
|
||||||
(void)offset;
|
|
||||||
(void)fi;
|
|
||||||
|
|
||||||
if ((fd = openat(priv->dirfd, pathname, O_RDONLY | O_DIRECTORY | O_CLOEXEC)) == -1)
|
|
||||||
return -errno;
|
|
||||||
if ((dp = fdopendir(fd)) == NULL) {
|
|
||||||
close(fd);
|
|
||||||
return -errno;
|
|
||||||
}
|
|
||||||
|
|
||||||
errno = 0; /* for the next readdir call */
|
|
||||||
while ((de = readdir(dp)) != NULL) {
|
|
||||||
if (flags & FUSE_READDIR_PLUS) {
|
|
||||||
if (fstatat(dirfd(dp), de->d_name, &st, AT_SYMLINK_NOFOLLOW) == -1) {
|
|
||||||
ret = -errno;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((ret = impl_getattr(ctx, &st)) < 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
errno = 0;
|
|
||||||
ret = filler(buf, de->d_name, &st, 0, FUSE_FILL_DIR_PLUS);
|
|
||||||
} else
|
|
||||||
ret = filler(buf, de->d_name, NULL, 0, 0);
|
|
||||||
|
|
||||||
if (ret != 0) {
|
|
||||||
ret = errno != 0 ? -errno : -EIO; /* filler */
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
errno = 0; /* for the next readdir call */
|
|
||||||
}
|
|
||||||
if (ret == 0 && errno != 0)
|
|
||||||
ret = -errno; /* readdir */
|
|
||||||
|
|
||||||
closedir(dp);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
int sharefs_mkdir(const char *pathname, mode_t mode) {
|
|
||||||
struct fuse_context *ctx;
|
|
||||||
struct sharefs_private *priv;
|
|
||||||
GET_CONTEXT_PRIV(ctx, priv);
|
|
||||||
MUST_TRANSLATE_PATHNAME(pathname);
|
|
||||||
|
|
||||||
(void)mode;
|
|
||||||
|
|
||||||
if (mkdirat(priv->dirfd, pathname, SHAREFS_PERM_DIR) == -1)
|
|
||||||
return -errno;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int sharefs_unlink(const char *pathname) {
|
|
||||||
struct fuse_context *ctx;
|
|
||||||
struct sharefs_private *priv;
|
|
||||||
GET_CONTEXT_PRIV(ctx, priv);
|
|
||||||
MUST_TRANSLATE_PATHNAME(pathname);
|
|
||||||
|
|
||||||
if (unlinkat(priv->dirfd, pathname, 0) == -1)
|
|
||||||
return -errno;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int sharefs_rmdir(const char *pathname) {
|
|
||||||
struct fuse_context *ctx;
|
|
||||||
struct sharefs_private *priv;
|
|
||||||
GET_CONTEXT_PRIV(ctx, priv);
|
|
||||||
MUST_TRANSLATE_PATHNAME(pathname);
|
|
||||||
|
|
||||||
if (unlinkat(priv->dirfd, pathname, AT_REMOVEDIR) == -1)
|
|
||||||
return -errno;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int sharefs_rename(const char *oldpath, const char *newpath, unsigned int flags) {
|
|
||||||
struct fuse_context *ctx;
|
|
||||||
struct sharefs_private *priv;
|
|
||||||
GET_CONTEXT_PRIV(ctx, priv);
|
|
||||||
MUST_TRANSLATE_PATHNAME(oldpath);
|
|
||||||
MUST_TRANSLATE_PATHNAME(newpath);
|
|
||||||
|
|
||||||
/* TODO(ophestra): replace with wrapper after 05ce67fea99ca09cd4b6625cff7aec9cc222dd5a reaches a release */
|
|
||||||
if (syscall(__NR_renameat2, priv->dirfd, oldpath, priv->dirfd, newpath, flags) == -1)
|
|
||||||
return -errno;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int sharefs_truncate(const char *pathname, off_t length, struct fuse_file_info *fi) {
|
|
||||||
int fd;
|
|
||||||
int ret;
|
|
||||||
|
|
||||||
struct fuse_context *ctx;
|
|
||||||
struct sharefs_private *priv;
|
|
||||||
GET_CONTEXT_PRIV(ctx, priv);
|
|
||||||
MUST_TRANSLATE_PATHNAME(pathname);
|
|
||||||
|
|
||||||
(void)fi;
|
|
||||||
|
|
||||||
if ((fd = openat(priv->dirfd, pathname, O_WRONLY | O_CLOEXEC)) == -1)
|
|
||||||
return -errno;
|
|
||||||
if ((ret = ftruncate(fd, length)) == -1)
|
|
||||||
ret = -errno;
|
|
||||||
close(fd);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
int sharefs_utimens(const char *pathname, const struct timespec times[2], struct fuse_file_info *fi) {
|
|
||||||
struct fuse_context *ctx;
|
|
||||||
struct sharefs_private *priv;
|
|
||||||
GET_CONTEXT_PRIV(ctx, priv);
|
|
||||||
MUST_TRANSLATE_PATHNAME(pathname);
|
|
||||||
|
|
||||||
(void)fi;
|
|
||||||
|
|
||||||
if (utimensat(priv->dirfd, pathname, times, AT_SYMLINK_NOFOLLOW) == -1)
|
|
||||||
return -errno;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int sharefs_create(const char *pathname, mode_t mode, struct fuse_file_info *fi) {
|
|
||||||
int fd;
|
|
||||||
|
|
||||||
struct fuse_context *ctx;
|
|
||||||
struct sharefs_private *priv;
|
|
||||||
GET_CONTEXT_PRIV(ctx, priv);
|
|
||||||
MUST_TRANSLATE_PATHNAME(pathname);
|
|
||||||
|
|
||||||
(void)mode;
|
|
||||||
|
|
||||||
if ((fd = openat(priv->dirfd, pathname, fi->flags & ~SHAREFS_FORBIDDEN_FLAGS, SHAREFS_PERM_REG)) == -1)
|
|
||||||
return -errno;
|
|
||||||
fi->fh = fd;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int sharefs_open(const char *pathname, struct fuse_file_info *fi) {
|
|
||||||
int fd;
|
|
||||||
|
|
||||||
struct fuse_context *ctx;
|
|
||||||
struct sharefs_private *priv;
|
|
||||||
GET_CONTEXT_PRIV(ctx, priv);
|
|
||||||
MUST_TRANSLATE_PATHNAME(pathname);
|
|
||||||
|
|
||||||
if ((fd = openat(priv->dirfd, pathname, fi->flags & ~SHAREFS_FORBIDDEN_FLAGS)) == -1)
|
|
||||||
return -errno;
|
|
||||||
fi->fh = fd;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset, struct fuse_file_info *fi) {
|
|
||||||
int ret;
|
|
||||||
|
|
||||||
(void)pathname;
|
|
||||||
|
|
||||||
if ((ret = pread(fi->fh, buf, count, offset)) == -1)
|
|
||||||
return -errno;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
int sharefs_write(const char *pathname, const char *buf, size_t count, off_t offset, struct fuse_file_info *fi) {
|
|
||||||
int ret;
|
|
||||||
|
|
||||||
(void)pathname;
|
|
||||||
|
|
||||||
if ((ret = pwrite(fi->fh, buf, count, offset)) == -1)
|
|
||||||
return -errno;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
int sharefs_statfs(const char *pathname, struct statvfs *statbuf) {
|
|
||||||
int fd;
|
|
||||||
int ret;
|
|
||||||
|
|
||||||
struct fuse_context *ctx;
|
|
||||||
struct sharefs_private *priv;
|
|
||||||
GET_CONTEXT_PRIV(ctx, priv);
|
|
||||||
MUST_TRANSLATE_PATHNAME(pathname);
|
|
||||||
|
|
||||||
if ((fd = openat(priv->dirfd, pathname, O_RDONLY | O_CLOEXEC)) == -1)
|
|
||||||
return -errno;
|
|
||||||
if ((ret = fstatvfs(fd, statbuf)) == -1)
|
|
||||||
ret = -errno;
|
|
||||||
close(fd);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
int sharefs_release(const char *pathname, struct fuse_file_info *fi) {
|
|
||||||
(void)pathname;
|
|
||||||
|
|
||||||
return close(fi->fh);
|
|
||||||
}
|
|
||||||
|
|
||||||
int sharefs_fsync(const char *pathname, int datasync, struct fuse_file_info *fi) {
|
|
||||||
(void)pathname;
|
|
||||||
|
|
||||||
if (datasync ? fdatasync(fi->fh) : fsync(fi->fh) == -1)
|
|
||||||
return -errno;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 12)
|
|
||||||
#include <fuse.h>
|
|
||||||
#include <fuse_lowlevel.h> /* for fuse_cmdline_help */
|
|
||||||
|
|
||||||
#if (FUSE_VERSION < FUSE_MAKE_VERSION(3, 12))
|
|
||||||
#error This package requires libfuse >= v3.12
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define SHAREFS_MEDIA_RW_ID (1 << 10) - 1 /* owning gid presented to userspace */
|
|
||||||
#define SHAREFS_PERM_DIR 0770 /* permission bits for directories presented to userspace */
|
|
||||||
#define SHAREFS_PERM_REG 0660 /* permission bits for regular files presented to userspace */
|
|
||||||
#define SHAREFS_FORBIDDEN_FLAGS O_DIRECT /* these open flags are cleared unconditionally */
|
|
||||||
|
|
||||||
/* sharefs_private is populated by sharefs_init and contains process-wide context */
|
|
||||||
struct sharefs_private {
|
|
||||||
int dirfd; /* source dirfd opened during sharefs_init */
|
|
||||||
uintptr_t setup; /* cgo handle of opaque setup state */
|
|
||||||
};
|
|
||||||
|
|
||||||
int sharefs_getattr(const char *pathname, struct stat *statbuf, struct fuse_file_info *fi);
|
|
||||||
int sharefs_readdir(const char *pathname, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags);
|
|
||||||
int sharefs_mkdir(const char *pathname, mode_t mode);
|
|
||||||
int sharefs_unlink(const char *pathname);
|
|
||||||
int sharefs_rmdir(const char *pathname);
|
|
||||||
int sharefs_rename(const char *oldpath, const char *newpath, unsigned int flags);
|
|
||||||
int sharefs_truncate(const char *pathname, off_t length, struct fuse_file_info *fi);
|
|
||||||
int sharefs_utimens(const char *pathname, const struct timespec times[2], struct fuse_file_info *fi);
|
|
||||||
int sharefs_create(const char *pathname, mode_t mode, struct fuse_file_info *fi);
|
|
||||||
int sharefs_open(const char *pathname, struct fuse_file_info *fi);
|
|
||||||
int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset, struct fuse_file_info *fi);
|
|
||||||
int sharefs_write(const char *pathname, const char *buf, size_t count, off_t offset, struct fuse_file_info *fi);
|
|
||||||
int sharefs_statfs(const char *pathname, struct statvfs *statbuf);
|
|
||||||
int sharefs_release(const char *pathname, struct fuse_file_info *fi);
|
|
||||||
int sharefs_fsync(const char *pathname, int datasync, struct fuse_file_info *fi);
|
|
||||||
@@ -1,570 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
/*
|
|
||||||
#cgo pkg-config: --static fuse3
|
|
||||||
|
|
||||||
#include "fuse-operations.h"
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
extern void *sharefs_init(struct fuse_conn_info *conn, struct fuse_config *cfg);
|
|
||||||
extern void sharefs_destroy(void *private_data);
|
|
||||||
|
|
||||||
typedef void (*closure)();
|
|
||||||
static inline struct fuse_opt _FUSE_OPT_END() { return (struct fuse_opt)FUSE_OPT_END; };
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/gob"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"runtime/cgo"
|
|
||||||
"strconv"
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
"hakurei.app/container"
|
|
||||||
"hakurei.app/container/std"
|
|
||||||
"hakurei.app/fhs"
|
|
||||||
"hakurei.app/hst"
|
|
||||||
"hakurei.app/internal/helper/proc"
|
|
||||||
"hakurei.app/internal/info"
|
|
||||||
"hakurei.app/message"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
// closure represents a C function pointer.
|
|
||||||
closure = C.closure
|
|
||||||
|
|
||||||
// fuseArgs represents the fuse_args structure.
|
|
||||||
fuseArgs = C.struct_fuse_args
|
|
||||||
|
|
||||||
// setupState holds state used for setup. Its cgo handle is included in
|
|
||||||
// sharefs_private and considered opaque to non-setup callbacks.
|
|
||||||
setupState struct {
|
|
||||||
// Whether sharefs_init failed.
|
|
||||||
initFailed bool
|
|
||||||
|
|
||||||
// Whether to create source directory as root.
|
|
||||||
mkdir bool
|
|
||||||
|
|
||||||
// Open file descriptor to fuse.
|
|
||||||
Fuse int
|
|
||||||
|
|
||||||
// Pathname to open for dirfd.
|
|
||||||
Source *check.Absolute
|
|
||||||
// New uid and gid to set by sharefs_init when starting as root.
|
|
||||||
Setuid, Setgid int
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() { gob.Register(new(setupState)) }
|
|
||||||
|
|
||||||
// destroySetup invalidates the setup [cgo.Handle] in a sharefs_private structure.
|
|
||||||
func destroySetup(private_data unsafe.Pointer) (ok bool) {
|
|
||||||
if private_data == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
priv := (*C.struct_sharefs_private)(private_data)
|
|
||||||
|
|
||||||
if h := cgo.Handle(priv.setup); h != 0 {
|
|
||||||
priv.setup = 0
|
|
||||||
h.Delete()
|
|
||||||
ok = true
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//export sharefs_init
|
|
||||||
func sharefs_init(
|
|
||||||
_ *C.struct_fuse_conn_info,
|
|
||||||
cfg *C.struct_fuse_config,
|
|
||||||
) unsafe.Pointer {
|
|
||||||
ctx := C.fuse_get_context()
|
|
||||||
priv := (*C.struct_sharefs_private)(ctx.private_data)
|
|
||||||
setup := cgo.Handle(priv.setup).Value().(*setupState)
|
|
||||||
|
|
||||||
if os.Geteuid() == 0 {
|
|
||||||
log.Println("filesystem daemon must not run as root")
|
|
||||||
goto fail
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.use_ino = C.true
|
|
||||||
cfg.direct_io = C.false
|
|
||||||
// getattr is context-dependent
|
|
||||||
cfg.attr_timeout = 0
|
|
||||||
cfg.entry_timeout = 0
|
|
||||||
cfg.negative_timeout = 0
|
|
||||||
|
|
||||||
// all future filesystem operations happen through this dirfd
|
|
||||||
if fd, err := syscall.Open(
|
|
||||||
setup.Source.String(),
|
|
||||||
syscall.O_DIRECTORY|syscall.O_RDONLY|syscall.O_CLOEXEC,
|
|
||||||
0,
|
|
||||||
); err != nil {
|
|
||||||
log.Printf("cannot open %q: %v", setup.Source, err)
|
|
||||||
goto fail
|
|
||||||
} else if err = syscall.Fchdir(fd); err != nil {
|
|
||||||
_ = syscall.Close(fd)
|
|
||||||
log.Printf("cannot enter %q: %s", setup.Source, err)
|
|
||||||
goto fail
|
|
||||||
} else {
|
|
||||||
priv.dirfd = C.int(fd)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.private_data
|
|
||||||
|
|
||||||
fail:
|
|
||||||
setup.initFailed = true
|
|
||||||
C.fuse_exit(ctx.fuse)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//export sharefs_destroy
|
|
||||||
func sharefs_destroy(private_data unsafe.Pointer) {
|
|
||||||
if private_data != nil {
|
|
||||||
destroySetup(private_data)
|
|
||||||
priv := (*C.struct_sharefs_private)(private_data)
|
|
||||||
|
|
||||||
if err := syscall.Close(int(priv.dirfd)); err != nil {
|
|
||||||
log.Printf("cannot close source directory: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// showHelp prints the help message.
|
|
||||||
func showHelp(args *fuseArgs) {
|
|
||||||
executableName := sharefsName
|
|
||||||
if args.argc > 0 {
|
|
||||||
executableName = filepath.Base(C.GoString(*args.argv))
|
|
||||||
} else if name, err := os.Executable(); err == nil {
|
|
||||||
executableName = filepath.Base(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("usage: %s [options] <mountpoint>\n\n", executableName)
|
|
||||||
|
|
||||||
fmt.Println("Filesystem options:")
|
|
||||||
fmt.Println(" -o source=/data/media source directory to be mounted")
|
|
||||||
fmt.Println(" -o setuid=1023 uid to run as when starting as root")
|
|
||||||
fmt.Println(" -o setgid=1023 gid to run as when starting as root")
|
|
||||||
|
|
||||||
fmt.Println("\nFUSE options:")
|
|
||||||
C.fuse_cmdline_help()
|
|
||||||
C.fuse_lib_help(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseOpts parses fuse options via fuse_opt_parse.
|
|
||||||
func parseOpts(args *fuseArgs, setup *setupState, log *log.Logger) (ok bool) {
|
|
||||||
var unsafeOpts struct {
|
|
||||||
// Pathname to writable source directory.
|
|
||||||
source *C.char
|
|
||||||
|
|
||||||
// Whether to create source directory as root.
|
|
||||||
mkdir C.int
|
|
||||||
|
|
||||||
// Decimal string representation of uid to set when running as root.
|
|
||||||
setuid *C.char
|
|
||||||
// Decimal string representation of gid to set when running as root.
|
|
||||||
setgid *C.char
|
|
||||||
|
|
||||||
// Decimal string representation of open file descriptor to read
|
|
||||||
// setupState from.
|
|
||||||
//
|
|
||||||
// This is an internal detail for containerisation and must not be
|
|
||||||
// specified directly.
|
|
||||||
setup *C.char
|
|
||||||
}
|
|
||||||
|
|
||||||
if C.fuse_opt_parse(args, unsafe.Pointer(&unsafeOpts), &[]C.struct_fuse_opt{
|
|
||||||
{templ: C.CString("source=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.source)), value: 0},
|
|
||||||
{templ: C.CString("mkdir"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.mkdir)), value: 1},
|
|
||||||
{templ: C.CString("setuid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setuid)), value: 0},
|
|
||||||
{templ: C.CString("setgid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setgid)), value: 0},
|
|
||||||
|
|
||||||
{templ: C.CString("setup=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setup)), value: 0},
|
|
||||||
|
|
||||||
C._FUSE_OPT_END(),
|
|
||||||
}[0], nil) == -1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if unsafeOpts.source != nil {
|
|
||||||
defer C.free(unsafe.Pointer(unsafeOpts.source))
|
|
||||||
}
|
|
||||||
if unsafeOpts.setuid != nil {
|
|
||||||
defer C.free(unsafe.Pointer(unsafeOpts.setuid))
|
|
||||||
}
|
|
||||||
if unsafeOpts.setgid != nil {
|
|
||||||
defer C.free(unsafe.Pointer(unsafeOpts.setgid))
|
|
||||||
}
|
|
||||||
|
|
||||||
if unsafeOpts.setup != nil {
|
|
||||||
defer C.free(unsafe.Pointer(unsafeOpts.setup))
|
|
||||||
|
|
||||||
if v, err := strconv.Atoi(C.GoString(unsafeOpts.setup)); err != nil || v < 3 {
|
|
||||||
log.Println("invalid value for option setup")
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
r := os.NewFile(uintptr(v), "setup")
|
|
||||||
defer func() {
|
|
||||||
if err = r.Close(); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if err = gob.NewDecoder(r).Decode(setup); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if setup.Fuse < 3 {
|
|
||||||
log.Println("invalid file descriptor", setup.Fuse)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if unsafeOpts.source == nil {
|
|
||||||
showHelp(args)
|
|
||||||
return false
|
|
||||||
} else if a, err := check.NewAbs(C.GoString(unsafeOpts.source)); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
setup.Source = a
|
|
||||||
}
|
|
||||||
setup.mkdir = unsafeOpts.mkdir != 0
|
|
||||||
|
|
||||||
if unsafeOpts.setuid == nil {
|
|
||||||
setup.Setuid = -1
|
|
||||||
} else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setuid)); err != nil || v <= 0 {
|
|
||||||
log.Println("invalid value for option setuid")
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
setup.Setuid = v
|
|
||||||
}
|
|
||||||
if unsafeOpts.setgid == nil {
|
|
||||||
setup.Setgid = -1
|
|
||||||
} else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setgid)); err != nil || v <= 0 {
|
|
||||||
log.Println("invalid value for option setgid")
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
setup.Setgid = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// copyArgs returns a heap allocated copy of an argument slice in fuse_args
|
|
||||||
// representation.
|
|
||||||
func copyArgs(s ...string) fuseArgs {
|
|
||||||
if len(s) == 0 {
|
|
||||||
return fuseArgs{argc: 0, argv: nil, allocated: 0}
|
|
||||||
}
|
|
||||||
args := unsafe.Slice((**C.char)(C.malloc(C.size_t(uintptr(len(s))*unsafe.Sizeof(s[0])))), len(s))
|
|
||||||
for i, arg := range s {
|
|
||||||
args[i] = C.CString(arg)
|
|
||||||
}
|
|
||||||
return fuseArgs{argc: C.int(len(s)), argv: &args[0], allocated: 1}
|
|
||||||
}
|
|
||||||
|
|
||||||
// freeArgs frees the contents of argument list.
|
|
||||||
func freeArgs(args *fuseArgs) { C.fuse_opt_free_args(args) }
|
|
||||||
|
|
||||||
// unsafeAddArgument adds an argument to fuseArgs via fuse_opt_add_arg.
|
|
||||||
//
|
|
||||||
// The last byte of arg must be 0.
|
|
||||||
func unsafeAddArgument(args *fuseArgs, arg string) {
|
|
||||||
C.fuse_opt_add_arg(args, (*C.char)(unsafe.Pointer(unsafe.StringData(arg))))
|
|
||||||
}
|
|
||||||
|
|
||||||
func _main(s ...string) (exitCode int) {
|
|
||||||
msg := message.New(log.Default())
|
|
||||||
container.TryArgv0(msg)
|
|
||||||
runtime.LockOSThread()
|
|
||||||
|
|
||||||
// don't mask creation mode, kernel already did that
|
|
||||||
syscall.Umask(0)
|
|
||||||
|
|
||||||
var pinner runtime.Pinner
|
|
||||||
defer pinner.Unpin()
|
|
||||||
|
|
||||||
args := copyArgs(s...)
|
|
||||||
defer freeArgs(&args)
|
|
||||||
|
|
||||||
// this causes the kernel to enforce access control based on struct stat
|
|
||||||
// populated by sharefs_getattr
|
|
||||||
unsafeAddArgument(&args, "-odefault_permissions\x00")
|
|
||||||
|
|
||||||
var priv C.struct_sharefs_private
|
|
||||||
pinner.Pin(&priv)
|
|
||||||
var setup setupState
|
|
||||||
priv.setup = C.uintptr_t(cgo.NewHandle(&setup))
|
|
||||||
defer destroySetup(unsafe.Pointer(&priv))
|
|
||||||
|
|
||||||
var opts C.struct_fuse_cmdline_opts
|
|
||||||
if C.fuse_parse_cmdline(&args, &opts) != 0 {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if opts.mountpoint != nil {
|
|
||||||
defer C.free(unsafe.Pointer(opts.mountpoint))
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.show_version != 0 {
|
|
||||||
fmt.Println("hakurei version", info.Version())
|
|
||||||
fmt.Println("FUSE library version", C.GoString(C.fuse_pkgversion()))
|
|
||||||
C.fuse_lowlevel_version()
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.show_help != 0 {
|
|
||||||
showHelp(&args)
|
|
||||||
return 0
|
|
||||||
} else if opts.mountpoint == nil {
|
|
||||||
log.Println("no mountpoint specified")
|
|
||||||
return 2
|
|
||||||
} else {
|
|
||||||
// hack to keep fuse_parse_cmdline happy in the container
|
|
||||||
mountpoint := C.GoString(opts.mountpoint)
|
|
||||||
pathnameArg := -1
|
|
||||||
for i, arg := range s {
|
|
||||||
if arg == mountpoint {
|
|
||||||
pathnameArg = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if pathnameArg < 0 {
|
|
||||||
log.Println("mountpoint must be absolute")
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
s[pathnameArg] = container.Nonexistent
|
|
||||||
}
|
|
||||||
|
|
||||||
if !parseOpts(&args, &setup, msg.GetLogger()) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
asRoot := os.Geteuid() == 0
|
|
||||||
|
|
||||||
if asRoot {
|
|
||||||
if setup.Setuid <= 0 || setup.Setgid <= 0 {
|
|
||||||
log.Println("setuid and setgid must not be 0")
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if setup.Fuse >= 3 {
|
|
||||||
log.Println("filesystem daemon must not run as root")
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if setup.mkdir {
|
|
||||||
if err := os.MkdirAll(setup.Source.String(), 0700); err != nil {
|
|
||||||
if !errors.Is(err, os.ErrExist) {
|
|
||||||
log.Println(err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
// skip setup for existing source directory
|
|
||||||
} else if err = os.Chown(setup.Source.String(), setup.Setuid, setup.Setgid); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if setup.Fuse < 3 && (setup.Setuid > 0 || setup.Setgid > 0) {
|
|
||||||
log.Println("setuid and setgid has no effect when not starting as root")
|
|
||||||
return 1
|
|
||||||
} else if setup.mkdir {
|
|
||||||
log.Println("mkdir has no effect when not starting as root")
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
op := C.struct_fuse_operations{
|
|
||||||
init: closure(C.sharefs_init),
|
|
||||||
destroy: closure(C.sharefs_destroy),
|
|
||||||
|
|
||||||
// implemented in fuse-helper.c
|
|
||||||
getattr: closure(C.sharefs_getattr),
|
|
||||||
readdir: closure(C.sharefs_readdir),
|
|
||||||
mkdir: closure(C.sharefs_mkdir),
|
|
||||||
unlink: closure(C.sharefs_unlink),
|
|
||||||
rmdir: closure(C.sharefs_rmdir),
|
|
||||||
rename: closure(C.sharefs_rename),
|
|
||||||
truncate: closure(C.sharefs_truncate),
|
|
||||||
utimens: closure(C.sharefs_utimens),
|
|
||||||
create: closure(C.sharefs_create),
|
|
||||||
open: closure(C.sharefs_open),
|
|
||||||
read: closure(C.sharefs_read),
|
|
||||||
write: closure(C.sharefs_write),
|
|
||||||
statfs: closure(C.sharefs_statfs),
|
|
||||||
release: closure(C.sharefs_release),
|
|
||||||
fsync: closure(C.sharefs_fsync),
|
|
||||||
}
|
|
||||||
|
|
||||||
fuse := C.fuse_new_fn(&args, &op, C.size_t(unsafe.Sizeof(op)), unsafe.Pointer(&priv))
|
|
||||||
if fuse == nil {
|
|
||||||
return 3
|
|
||||||
}
|
|
||||||
defer C.fuse_destroy(fuse)
|
|
||||||
se := C.fuse_get_session(fuse)
|
|
||||||
|
|
||||||
if setup.Fuse < 3 {
|
|
||||||
// unconfined, set up mount point and container
|
|
||||||
if C.fuse_mount(fuse, opts.mountpoint) != 0 {
|
|
||||||
return 4
|
|
||||||
}
|
|
||||||
// unmounted by initial process
|
|
||||||
defer func() {
|
|
||||||
if exitCode == 5 {
|
|
||||||
C.fuse_unmount(fuse)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if asRoot {
|
|
||||||
if err := syscall.Setresgid(setup.Setgid, setup.Setgid, setup.Setgid); err != nil {
|
|
||||||
log.Printf("cannot set gid: %v", err)
|
|
||||||
return 5
|
|
||||||
}
|
|
||||||
if err := syscall.Setgroups(nil); err != nil {
|
|
||||||
log.Printf("cannot set supplementary groups: %v", err)
|
|
||||||
return 5
|
|
||||||
}
|
|
||||||
if err := syscall.Setresuid(setup.Setuid, setup.Setuid, setup.Setuid); err != nil {
|
|
||||||
log.Printf("cannot set uid: %v", err)
|
|
||||||
return 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
msg.SwapVerbose(opts.debug != 0)
|
|
||||||
ctx := context.Background()
|
|
||||||
if opts.foreground != 0 {
|
|
||||||
c, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
defer cancel()
|
|
||||||
ctx = c
|
|
||||||
}
|
|
||||||
z := container.New(ctx, msg)
|
|
||||||
z.AllowOrphan = opts.foreground == 0
|
|
||||||
z.Env = os.Environ()
|
|
||||||
|
|
||||||
// keep fuse_parse_cmdline happy in the container
|
|
||||||
z.Tmpfs(check.MustAbs(container.Nonexistent), 1<<10, 0755)
|
|
||||||
|
|
||||||
z.Path = fhs.AbsProcSelfExe
|
|
||||||
z.Args = s
|
|
||||||
z.ForwardCancel = true
|
|
||||||
z.SeccompPresets |= std.PresetStrict
|
|
||||||
z.ParentPerm = 0700
|
|
||||||
z.Bind(setup.Source, setup.Source, std.BindWritable)
|
|
||||||
if !z.AllowOrphan {
|
|
||||||
z.WaitDelay = hst.WaitDelayMax
|
|
||||||
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
|
|
||||||
}
|
|
||||||
z.Bind(z.Path, z.Path, 0)
|
|
||||||
setup.Fuse = int(proc.ExtraFileSlice(
|
|
||||||
&z.ExtraFiles,
|
|
||||||
os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse"),
|
|
||||||
))
|
|
||||||
|
|
||||||
var setupPipe [2]*os.File
|
|
||||||
if r, w, err := os.Pipe(); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return 5
|
|
||||||
} else {
|
|
||||||
z.Args = append(z.Args, "-osetup="+strconv.Itoa(3+len(z.ExtraFiles)))
|
|
||||||
z.ExtraFiles = append(z.ExtraFiles, r)
|
|
||||||
setupPipe[0], setupPipe[1] = r, w
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := z.Start(); err != nil {
|
|
||||||
if m, ok := message.GetMessage(err); ok {
|
|
||||||
log.Println(m)
|
|
||||||
} else {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
return 5
|
|
||||||
}
|
|
||||||
if err := setupPipe[0].Close(); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
if err := z.Serve(); err != nil {
|
|
||||||
if m, ok := message.GetMessage(err); ok {
|
|
||||||
log.Println(m)
|
|
||||||
} else {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
return 5
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := gob.NewEncoder(setupPipe[1]).Encode(&setup); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return 5
|
|
||||||
} else if err = setupPipe[1].Close(); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !z.AllowOrphan {
|
|
||||||
if err := z.Wait(); err != nil {
|
|
||||||
var exitError *exec.ExitError
|
|
||||||
if !errors.As(err, &exitError) || exitError == nil {
|
|
||||||
log.Println(err)
|
|
||||||
return 5
|
|
||||||
}
|
|
||||||
switch code := exitError.ExitCode(); syscall.Signal(code & 0x7f) {
|
|
||||||
case syscall.SIGINT:
|
|
||||||
case syscall.SIGTERM:
|
|
||||||
|
|
||||||
default:
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
} else { // confined
|
|
||||||
C.free(unsafe.Pointer(opts.mountpoint))
|
|
||||||
// must be heap allocated
|
|
||||||
opts.mountpoint = C.CString("/dev/fd/" + strconv.Itoa(setup.Fuse))
|
|
||||||
|
|
||||||
if err := os.Chdir("/"); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if C.fuse_mount(fuse, opts.mountpoint) != 0 {
|
|
||||||
return 4
|
|
||||||
}
|
|
||||||
defer C.fuse_unmount(fuse)
|
|
||||||
|
|
||||||
if C.fuse_set_signal_handlers(se) != 0 {
|
|
||||||
return 6
|
|
||||||
}
|
|
||||||
defer C.fuse_remove_signal_handlers(se)
|
|
||||||
|
|
||||||
if opts.singlethread != 0 {
|
|
||||||
if C.fuse_loop(fuse) != 0 {
|
|
||||||
return 8
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
loopConfig := C.fuse_loop_cfg_create()
|
|
||||||
if loopConfig == nil {
|
|
||||||
return 7
|
|
||||||
}
|
|
||||||
defer C.fuse_loop_cfg_destroy(loopConfig)
|
|
||||||
|
|
||||||
C.fuse_loop_cfg_set_clone_fd(loopConfig, C.uint(opts.clone_fd))
|
|
||||||
|
|
||||||
C.fuse_loop_cfg_set_idle_threads(loopConfig, opts.max_idle_threads)
|
|
||||||
C.fuse_loop_cfg_set_max_threads(loopConfig, opts.max_threads)
|
|
||||||
if C.fuse_loop_mt(fuse, loopConfig) != 0 {
|
|
||||||
return 8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if setup.initFailed {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"log"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseOpts(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
args []string
|
|
||||||
want setupState
|
|
||||||
wantLog string
|
|
||||||
wantOk bool
|
|
||||||
}{
|
|
||||||
{"zero length", []string{}, setupState{}, "", false},
|
|
||||||
|
|
||||||
{"not absolute", []string{"sharefs",
|
|
||||||
"-o", "source=nonexistent",
|
|
||||||
"-o", "setuid=1023",
|
|
||||||
"-o", "setgid=1023",
|
|
||||||
}, setupState{}, "sharefs: path \"nonexistent\" is not absolute\n", false},
|
|
||||||
|
|
||||||
{"not specified", []string{"sharefs",
|
|
||||||
"-o", "setuid=1023",
|
|
||||||
"-o", "setgid=1023",
|
|
||||||
}, setupState{}, "", false},
|
|
||||||
|
|
||||||
{"invalid setuid", []string{"sharefs",
|
|
||||||
"-o", "source=/proc/nonexistent",
|
|
||||||
"-o", "setuid=ff",
|
|
||||||
"-o", "setgid=1023",
|
|
||||||
}, setupState{
|
|
||||||
Source: check.MustAbs("/proc/nonexistent"),
|
|
||||||
}, "sharefs: invalid value for option setuid\n", false},
|
|
||||||
|
|
||||||
{"invalid setgid", []string{"sharefs",
|
|
||||||
"-o", "source=/proc/nonexistent",
|
|
||||||
"-o", "setuid=1023",
|
|
||||||
"-o", "setgid=ff",
|
|
||||||
}, setupState{
|
|
||||||
Source: check.MustAbs("/proc/nonexistent"),
|
|
||||||
Setuid: 1023,
|
|
||||||
}, "sharefs: invalid value for option setgid\n", false},
|
|
||||||
|
|
||||||
{"simple", []string{"sharefs",
|
|
||||||
"-o", "source=/proc/nonexistent",
|
|
||||||
}, setupState{
|
|
||||||
Source: check.MustAbs("/proc/nonexistent"),
|
|
||||||
Setuid: -1,
|
|
||||||
Setgid: -1,
|
|
||||||
}, "", true},
|
|
||||||
|
|
||||||
{"root", []string{"sharefs",
|
|
||||||
"-o", "source=/proc/nonexistent",
|
|
||||||
"-o", "setuid=1023",
|
|
||||||
"-o", "setgid=1023",
|
|
||||||
}, setupState{
|
|
||||||
Source: check.MustAbs("/proc/nonexistent"),
|
|
||||||
Setuid: 1023,
|
|
||||||
Setgid: 1023,
|
|
||||||
}, "", true},
|
|
||||||
|
|
||||||
{"setuid", []string{"sharefs",
|
|
||||||
"-o", "source=/proc/nonexistent",
|
|
||||||
"-o", "setuid=1023",
|
|
||||||
}, setupState{
|
|
||||||
Source: check.MustAbs("/proc/nonexistent"),
|
|
||||||
Setuid: 1023,
|
|
||||||
Setgid: -1,
|
|
||||||
}, "", true},
|
|
||||||
|
|
||||||
{"setgid", []string{"sharefs",
|
|
||||||
"-o", "source=/proc/nonexistent",
|
|
||||||
"-o", "setgid=1023",
|
|
||||||
}, setupState{
|
|
||||||
Source: check.MustAbs("/proc/nonexistent"),
|
|
||||||
Setuid: -1,
|
|
||||||
Setgid: 1023,
|
|
||||||
}, "", true},
|
|
||||||
}
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
got setupState
|
|
||||||
buf bytes.Buffer
|
|
||||||
)
|
|
||||||
args := copyArgs(tc.args...)
|
|
||||||
defer freeArgs(&args)
|
|
||||||
unsafeAddArgument(&args, "-odefault_permissions\x00")
|
|
||||||
|
|
||||||
if ok := parseOpts(&args, &got, log.New(&buf, "sharefs: ", 0)); ok != tc.wantOk {
|
|
||||||
t.Errorf("parseOpts: ok = %v, want %v", ok, tc.wantOk)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(&got, &tc.want) {
|
|
||||||
t.Errorf("parseOpts: setup = %#v, want %#v", got, tc.want)
|
|
||||||
}
|
|
||||||
|
|
||||||
if buf.String() != tc.wantLog {
|
|
||||||
t.Errorf("parseOpts: log =\n%s\nwant\n%s", buf.String(), tc.wantLog)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// The sharefs FUSE filesystem is a permissionless shared filesystem.
|
|
||||||
//
|
|
||||||
// This filesystem is the primary means of file sharing between hakurei
|
|
||||||
// application containers. It serves the same purpose in Rosa OS as /sdcard
|
|
||||||
// does in AOSP.
|
|
||||||
//
|
|
||||||
// See help message for all available options.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"slices"
|
|
||||||
)
|
|
||||||
|
|
||||||
// sharefsName is the prefix used by log.std in the sharefs process.
|
|
||||||
const sharefsName = "sharefs"
|
|
||||||
|
|
||||||
// handleMountArgs returns an alternative, libfuse-compatible args slice for
|
|
||||||
// args passed by mount -t fuse.sharefs [options] sharefs <mountpoint>.
|
|
||||||
//
|
|
||||||
// In this case, args always has a length of 5 with index 0 being what comes
|
|
||||||
// after "fuse." in the filesystem type, 1 is the uninterpreted string passed
|
|
||||||
// to mount (sharefsName is used as the magic string to enable this hack),
|
|
||||||
// 2 is passed through to libfuse as mountpoint, and 3 is always "-o".
|
|
||||||
func handleMountArgs(args []string) []string {
|
|
||||||
if len(args) == 5 && args[1] == sharefsName && args[3] == "-o" {
|
|
||||||
return []string{sharefsName, args[2], "-o", args[4]}
|
|
||||||
}
|
|
||||||
return slices.Clone(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
log.SetFlags(0)
|
|
||||||
log.SetPrefix(sharefsName + ": ")
|
|
||||||
|
|
||||||
os.Exit(_main(handleMountArgs(os.Args)...))
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"slices"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHandleMountArgs(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
args []string
|
|
||||||
want []string
|
|
||||||
}{
|
|
||||||
{"nil", nil, nil},
|
|
||||||
{"passthrough", []string{"sharefs", "-V"}, []string{"sharefs", "-V"}},
|
|
||||||
{"replace", []string{"/sbin/sharefs", "sharefs", "/sdcard", "-o", "rw"}, []string{"sharefs", "/sdcard", "-o", "rw"}},
|
|
||||||
}
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
if got := handleMountArgs(tc.args); !slices.Equal(got, tc.want) {
|
|
||||||
t.Errorf("handleMountArgs: %q, want %q", got, tc.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
{ pkgs, ... }:
|
|
||||||
{
|
|
||||||
users.users = {
|
|
||||||
alice = {
|
|
||||||
isNormalUser = true;
|
|
||||||
description = "Alice Foobar";
|
|
||||||
password = "foobar";
|
|
||||||
uid = 1000;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
home-manager.users.alice.home.stateVersion = "24.11";
|
|
||||||
|
|
||||||
# Automatically login on tty1 as a normal user:
|
|
||||||
services.getty.autologinUser = "alice";
|
|
||||||
|
|
||||||
environment = {
|
|
||||||
# For benchmarking sharefs:
|
|
||||||
systemPackages = [ pkgs.fsmark ];
|
|
||||||
};
|
|
||||||
|
|
||||||
virtualisation = {
|
|
||||||
# Hopefully reduces spurious test failures:
|
|
||||||
memorySize = if pkgs.stdenv.hostPlatform.is32bit then 2046 else 8192;
|
|
||||||
|
|
||||||
diskSize = 6 * 1024;
|
|
||||||
|
|
||||||
qemu.options = [
|
|
||||||
# Increase test performance:
|
|
||||||
"-smp 16"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
environment.hakurei = rec {
|
|
||||||
enable = true;
|
|
||||||
stateDir = "/var/lib/hakurei";
|
|
||||||
sharefs.source = "${stateDir}/sdcard";
|
|
||||||
users.alice = 0;
|
|
||||||
|
|
||||||
extraHomeConfig = {
|
|
||||||
home.stateVersion = "23.05";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
{
|
|
||||||
testers,
|
|
||||||
|
|
||||||
system,
|
|
||||||
self,
|
|
||||||
}:
|
|
||||||
testers.nixosTest {
|
|
||||||
name = "sharefs";
|
|
||||||
nodes.machine =
|
|
||||||
{ options, pkgs, ... }:
|
|
||||||
let
|
|
||||||
fhs =
|
|
||||||
let
|
|
||||||
hakurei = options.environment.hakurei.package.default;
|
|
||||||
in
|
|
||||||
pkgs.buildFHSEnv {
|
|
||||||
pname = "hakurei-fhs";
|
|
||||||
inherit (hakurei) version;
|
|
||||||
targetPkgs = _: hakurei.targetPkgs;
|
|
||||||
extraOutputsToInstall = [ "dev" ];
|
|
||||||
profile = ''
|
|
||||||
export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
environment.systemPackages = [
|
|
||||||
# For go tests:
|
|
||||||
(pkgs.writeShellScriptBin "sharefs-workload-hakurei-tests" ''
|
|
||||||
cp -r "${self.packages.${system}.hakurei.src}" "/sdcard/hakurei" && cd "/sdcard/hakurei"
|
|
||||||
${fhs}/bin/hakurei-fhs -c 'ROSA_SKIP_BINFMT=1 CC="clang -O3 -Werror" go test ./...'
|
|
||||||
'')
|
|
||||||
];
|
|
||||||
|
|
||||||
imports = [
|
|
||||||
./configuration.nix
|
|
||||||
|
|
||||||
self.nixosModules.hakurei
|
|
||||||
self.inputs.home-manager.nixosModules.home-manager
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
testScript = builtins.readFile ./test.py;
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
//go:build raceattr
|
|
||||||
|
|
||||||
// The raceattr program reproduces vfs inode file attribute race.
|
|
||||||
//
|
|
||||||
// Even though libfuse high-level API presents the address of a struct stat
|
|
||||||
// alongside struct fuse_context, file attributes are actually inherent to the
|
|
||||||
// inode, instead of the specific call from userspace. The kernel implementation
|
|
||||||
// in fs/fuse/xattr.c appears to make stale data in the inode (set by a previous
|
|
||||||
// call) impossible or very unlikely to reach userspace via the stat family of
|
|
||||||
// syscalls. However, when using default_permissions to have the VFS check
|
|
||||||
// permissions, this race still happens, despite the resulting struct stat being
|
|
||||||
// correct when overriding the check via capabilities otherwise.
|
|
||||||
//
|
|
||||||
// This program reproduces the failure, but because of its continuous nature, it
|
|
||||||
// is provided independent of the vm integration test suite.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"flag"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"runtime"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newStatAs(
|
|
||||||
ctx context.Context, cancel context.CancelFunc,
|
|
||||||
n *atomic.Uint64, ok *atomic.Bool,
|
|
||||||
uid uint32, pathname string,
|
|
||||||
continuous bool,
|
|
||||||
) func() {
|
|
||||||
return func() {
|
|
||||||
runtime.LockOSThread()
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if _, _, errno := syscall.Syscall(
|
|
||||||
syscall.SYS_SETUID, uintptr(uid),
|
|
||||||
0, 0,
|
|
||||||
); errno != 0 {
|
|
||||||
cancel()
|
|
||||||
log.Printf("cannot set uid to %d: %s", uid, errno)
|
|
||||||
}
|
|
||||||
|
|
||||||
var stat syscall.Stat_t
|
|
||||||
for {
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := syscall.Lstat(pathname, &stat); err != nil {
|
|
||||||
// SHAREFS_PERM_DIR not world executable, or
|
|
||||||
// SHAREFS_PERM_REG not world readable
|
|
||||||
if !continuous {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
ok.Store(true)
|
|
||||||
log.Printf("uid %d: %v", uid, err)
|
|
||||||
} else if stat.Uid != uid {
|
|
||||||
// appears to be unreachable
|
|
||||||
if !continuous {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
ok.Store(true)
|
|
||||||
log.Printf("got uid %d instead of %d", stat.Uid, uid)
|
|
||||||
}
|
|
||||||
n.Add(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
log.SetFlags(0)
|
|
||||||
log.SetPrefix("raceattr: ")
|
|
||||||
|
|
||||||
p := flag.String("target", "/sdcard/raceattr", "pathname of test file")
|
|
||||||
u0 := flag.Int("uid0", 1<<10-1, "first uid")
|
|
||||||
u1 := flag.Int("uid1", 1<<10-2, "second uid")
|
|
||||||
count := flag.Int("count", 1, "threads per uid")
|
|
||||||
continuous := flag.Bool("continuous", false, "keep running even after reproduce")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if os.Geteuid() != 0 {
|
|
||||||
log.Fatal("this program must run as root")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := signal.NotifyContext(
|
|
||||||
context.Background(),
|
|
||||||
syscall.SIGINT,
|
|
||||||
syscall.SIGTERM,
|
|
||||||
syscall.SIGHUP,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := os.WriteFile(*p, nil, 0); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
wg sync.WaitGroup
|
|
||||||
|
|
||||||
n atomic.Uint64
|
|
||||||
ok atomic.Bool
|
|
||||||
)
|
|
||||||
|
|
||||||
if *count < 1 {
|
|
||||||
*count = 1
|
|
||||||
}
|
|
||||||
for range *count {
|
|
||||||
wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u0), *p, *continuous))
|
|
||||||
if *u1 >= 0 {
|
|
||||||
wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u1), *p, *continuous))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
if !*continuous && ok.Load() {
|
|
||||||
log.Printf("reproduced after %d calls", n.Load())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
start_all()
|
|
||||||
machine.wait_for_unit("multi-user.target")
|
|
||||||
|
|
||||||
# To check sharefs version:
|
|
||||||
print(machine.succeed("sharefs -V"))
|
|
||||||
|
|
||||||
# Make sure sharefs started:
|
|
||||||
machine.wait_for_unit("sdcard.mount")
|
|
||||||
|
|
||||||
machine.succeed("mkdir /mnt")
|
|
||||||
def check_bad_opts_output(opts, want, source="/etc", privileged=False):
|
|
||||||
output = machine.fail(("" if privileged else "sudo -u alice -i ") + f"sharefs -f -o source={source},{opts} /mnt 2>&1")
|
|
||||||
if output != want:
|
|
||||||
raise Exception(f"unexpected output: {output}")
|
|
||||||
|
|
||||||
# Malformed setuid/setgid representation:
|
|
||||||
check_bad_opts_output("setuid=ff", "sharefs: invalid value for option setuid\n")
|
|
||||||
check_bad_opts_output("setgid=ff", "sharefs: invalid value for option setgid\n")
|
|
||||||
|
|
||||||
# Bounds check for setuid/setgid:
|
|
||||||
check_bad_opts_output("setuid=0", "sharefs: invalid value for option setuid\n")
|
|
||||||
check_bad_opts_output("setgid=0", "sharefs: invalid value for option setgid\n")
|
|
||||||
check_bad_opts_output("setuid=-1", "sharefs: invalid value for option setuid\n")
|
|
||||||
check_bad_opts_output("setgid=-1", "sharefs: invalid value for option setgid\n")
|
|
||||||
|
|
||||||
# Non-root setuid/setgid:
|
|
||||||
check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
|
||||||
check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
|
||||||
check_bad_opts_output("setuid=1023,setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
|
||||||
check_bad_opts_output("mkdir", "sharefs: mkdir has no effect when not starting as root\n")
|
|
||||||
|
|
||||||
# Starting as root without setuid/setgid:
|
|
||||||
check_bad_opts_output("allow_other", "sharefs: setuid and setgid must not be 0\n", privileged=True)
|
|
||||||
check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True)
|
|
||||||
check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True)
|
|
||||||
|
|
||||||
# Make sure nothing actually got mounted:
|
|
||||||
machine.fail("umount /mnt")
|
|
||||||
machine.succeed("rmdir /mnt")
|
|
||||||
|
|
||||||
# Unprivileged mount/unmount:
|
|
||||||
machine.succeed("sudo -u alice -i mkdir /home/alice/{sdcard,persistent}")
|
|
||||||
machine.succeed("sudo -u alice -i sharefs -o source=/home/alice/persistent /home/alice/sdcard")
|
|
||||||
machine.succeed("sudo -u alice -i touch /home/alice/sdcard/check")
|
|
||||||
machine.succeed("sudo -u alice -i umount /home/alice/sdcard")
|
|
||||||
machine.succeed("sudo -u alice -i rm /home/alice/persistent/check")
|
|
||||||
machine.succeed("sudo -u alice -i rmdir /home/alice/{sdcard,persistent}")
|
|
||||||
|
|
||||||
# Benchmark sharefs:
|
|
||||||
machine.succeed("fs_mark -v -d /sdcard/fs_mark -l /tmp/fs_log.txt")
|
|
||||||
machine.copy_from_vm("/tmp/fs_log.txt", "")
|
|
||||||
|
|
||||||
# Check permissions:
|
|
||||||
machine.succeed("sudo -u sharefs touch /var/lib/hakurei/sdcard/fs_mark/.check")
|
|
||||||
machine.succeed("sudo -u sharefs rm /var/lib/hakurei/sdcard/fs_mark/.check")
|
|
||||||
machine.succeed("sudo -u alice rm -rf /sdcard/fs_mark")
|
|
||||||
machine.fail("ls /var/lib/hakurei/sdcard/fs_mark")
|
|
||||||
|
|
||||||
# Run hakurei tests on sharefs:
|
|
||||||
machine.succeed("sudo -u alice -i sharefs-workload-hakurei-tests")
|
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestBuild(t *testing.T) {
|
func TestBuild(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
c := command.New(nil, nil, "test", nil)
|
c := command.New(nil, nil, "test", nil)
|
||||||
stubHandler := func([]string) error { panic("unreachable") }
|
stubHandler := func([]string) error { panic("unreachable") }
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestParse(t *testing.T) {
|
func TestParse(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
buildTree func(wout, wlog io.Writer) command.Command
|
buildTree func(wout, wlog io.Writer) command.Command
|
||||||
@@ -253,7 +251,6 @@ Commands:
|
|||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
wout, wlog := new(bytes.Buffer), new(bytes.Buffer)
|
wout, wlog := new(bytes.Buffer), new(bytes.Buffer)
|
||||||
c := tc.buildTree(wout, wlog)
|
c := tc.buildTree(wout, wlog)
|
||||||
|
|
||||||
|
|||||||
@@ -6,19 +6,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestParseUnreachable(t *testing.T) {
|
func TestParseUnreachable(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// top level bypasses name matching and recursive calls to Parse
|
// top level bypasses name matching and recursive calls to Parse
|
||||||
// returns when encountering zero-length args
|
// returns when encountering zero-length args
|
||||||
t.Run("zero-length args", func(t *testing.T) {
|
t.Run("zero-length args", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
defer checkRecover(t, "Parse", "attempted to parse with zero length args")
|
defer checkRecover(t, "Parse", "attempted to parse with zero length args")
|
||||||
_ = newNode(panicWriter{}, nil, " ", " ").Parse(nil)
|
_ = newNode(panicWriter{}, nil, " ", " ").Parse(nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
// top level must not have siblings
|
// top level must not have siblings
|
||||||
t.Run("toplevel siblings", func(t *testing.T) {
|
t.Run("toplevel siblings", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
defer checkRecover(t, "Parse", "invalid toplevel state")
|
defer checkRecover(t, "Parse", "invalid toplevel state")
|
||||||
n := newNode(panicWriter{}, nil, " ", "")
|
n := newNode(panicWriter{}, nil, " ", "")
|
||||||
n.append(newNode(panicWriter{}, nil, " ", " "))
|
n.append(newNode(panicWriter{}, nil, " ", " "))
|
||||||
@@ -27,7 +23,6 @@ func TestParseUnreachable(t *testing.T) {
|
|||||||
|
|
||||||
// a node with descendents must not have a direct handler
|
// a node with descendents must not have a direct handler
|
||||||
t.Run("sub handle conflict", func(t *testing.T) {
|
t.Run("sub handle conflict", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
defer checkRecover(t, "Parse", "invalid subcommand tree state")
|
defer checkRecover(t, "Parse", "invalid subcommand tree state")
|
||||||
n := newNode(panicWriter{}, nil, " ", " ")
|
n := newNode(panicWriter{}, nil, " ", " ")
|
||||||
n.adopt(newNode(panicWriter{}, nil, " ", " "))
|
n.adopt(newNode(panicWriter{}, nil, " ", " "))
|
||||||
@@ -37,7 +32,6 @@ func TestParseUnreachable(t *testing.T) {
|
|||||||
|
|
||||||
// this would only happen if a node was matched twice
|
// this would only happen if a node was matched twice
|
||||||
t.Run("parsed flag set", func(t *testing.T) {
|
t.Run("parsed flag set", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
defer checkRecover(t, "Parse", "invalid set state")
|
defer checkRecover(t, "Parse", "invalid set state")
|
||||||
n := newNode(panicWriter{}, nil, " ", "")
|
n := newNode(panicWriter{}, nil, " ", "")
|
||||||
set := flag.NewFlagSet("parsed", flag.ContinueOnError)
|
set := flag.NewFlagSet("parsed", flag.ContinueOnError)
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AbsoluteError is returned by [NewAbs] and holds the invalid pathname.
|
||||||
|
type AbsoluteError struct {
|
||||||
|
Pathname string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AbsoluteError) Error() string { return fmt.Sprintf("path %q is not absolute", e.Pathname) }
|
||||||
|
func (e *AbsoluteError) Is(target error) bool {
|
||||||
|
var ce *AbsoluteError
|
||||||
|
if !errors.As(target, &ce) {
|
||||||
|
return errors.Is(target, syscall.EINVAL)
|
||||||
|
}
|
||||||
|
return *e == *ce
|
||||||
|
}
|
||||||
|
|
||||||
|
// Absolute holds a pathname checked to be absolute.
|
||||||
|
type Absolute struct {
|
||||||
|
pathname string
|
||||||
|
}
|
||||||
|
|
||||||
|
// isAbs wraps [path.IsAbs] in case additional checks are added in the future.
|
||||||
|
func isAbs(pathname string) bool { return path.IsAbs(pathname) }
|
||||||
|
|
||||||
|
func (a *Absolute) String() string {
|
||||||
|
if a.pathname == zeroString {
|
||||||
|
panic("attempted use of zero Absolute")
|
||||||
|
}
|
||||||
|
return a.pathname
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Absolute) Is(v *Absolute) bool {
|
||||||
|
if a == nil && v == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return a != nil && v != nil &&
|
||||||
|
a.pathname != zeroString && v.pathname != zeroString &&
|
||||||
|
a.pathname == v.pathname
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAbs checks pathname and returns a new [Absolute] if pathname is absolute.
|
||||||
|
func NewAbs(pathname string) (*Absolute, error) {
|
||||||
|
if !isAbs(pathname) {
|
||||||
|
return nil, &AbsoluteError{pathname}
|
||||||
|
}
|
||||||
|
return &Absolute{pathname}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustAbs calls [NewAbs] and panics on error.
|
||||||
|
func MustAbs(pathname string) *Absolute {
|
||||||
|
if a, err := NewAbs(pathname); err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
} else {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append calls [path.Join] with [Absolute] as the first element.
|
||||||
|
func (a *Absolute) Append(elem ...string) *Absolute {
|
||||||
|
return &Absolute{path.Join(append([]string{a.String()}, elem...)...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dir calls [path.Dir] with [Absolute] as its argument.
|
||||||
|
func (a *Absolute) Dir() *Absolute { return &Absolute{path.Dir(a.String())} }
|
||||||
|
|
||||||
|
func (a *Absolute) GobEncode() ([]byte, error) { return []byte(a.String()), nil }
|
||||||
|
func (a *Absolute) GobDecode(data []byte) error {
|
||||||
|
pathname := string(data)
|
||||||
|
if !isAbs(pathname) {
|
||||||
|
return &AbsoluteError{pathname}
|
||||||
|
}
|
||||||
|
a.pathname = pathname
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Absolute) MarshalJSON() ([]byte, error) { return json.Marshal(a.String()) }
|
||||||
|
func (a *Absolute) UnmarshalJSON(data []byte) error {
|
||||||
|
var pathname string
|
||||||
|
if err := json.Unmarshal(data, &pathname); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !isAbs(pathname) {
|
||||||
|
return &AbsoluteError{pathname}
|
||||||
|
}
|
||||||
|
a.pathname = pathname
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortAbs calls [slices.SortFunc] for a slice of [Absolute].
|
||||||
|
func SortAbs(x []*Absolute) {
|
||||||
|
slices.SortFunc(x, func(a, b *Absolute) int { return strings.Compare(a.String(), b.String()) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompactAbs calls [slices.CompactFunc] for a slice of [Absolute].
|
||||||
|
func CompactAbs(s []*Absolute) []*Absolute {
|
||||||
|
return slices.CompactFunc(s, func(a *Absolute, b *Absolute) bool { return a.String() == b.String() })
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package check_test
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -9,19 +9,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
_ "unsafe" // for go:linkname
|
|
||||||
|
|
||||||
. "hakurei.app/check"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// unsafeAbs returns check.Absolute on any string value.
|
|
||||||
//
|
|
||||||
//go:linkname unsafeAbs hakurei.app/check.unsafeAbs
|
|
||||||
func unsafeAbs(pathname string) *Absolute
|
|
||||||
|
|
||||||
func TestAbsoluteError(t *testing.T) {
|
func TestAbsoluteError(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
||||||
@@ -31,8 +21,8 @@ func TestAbsoluteError(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{"EINVAL", new(AbsoluteError), syscall.EINVAL, true},
|
{"EINVAL", new(AbsoluteError), syscall.EINVAL, true},
|
||||||
{"not EINVAL", new(AbsoluteError), syscall.EBADE, false},
|
{"not EINVAL", new(AbsoluteError), syscall.EBADE, false},
|
||||||
{"ne val", new(AbsoluteError), AbsoluteError("etc"), false},
|
{"ne val", new(AbsoluteError), &AbsoluteError{"etc"}, false},
|
||||||
{"equals", AbsoluteError("etc"), AbsoluteError("etc"), true},
|
{"equals", &AbsoluteError{"etc"}, &AbsoluteError{"etc"}, true},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
@@ -42,18 +32,14 @@ func TestAbsoluteError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Run("string", func(t *testing.T) {
|
t.Run("string", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
want := `path "etc" is not absolute`
|
want := `path "etc" is not absolute`
|
||||||
if got := (AbsoluteError("etc")).Error(); got != want {
|
if got := (&AbsoluteError{"etc"}).Error(); got != want {
|
||||||
t.Errorf("Error: %q, want %q", got, want)
|
t.Errorf("Error: %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewAbs(t *testing.T) {
|
func TestNewAbs(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
||||||
@@ -62,14 +48,12 @@ func TestNewAbs(t *testing.T) {
|
|||||||
wantErr error
|
wantErr error
|
||||||
}{
|
}{
|
||||||
{"good", "/etc", MustAbs("/etc"), nil},
|
{"good", "/etc", MustAbs("/etc"), nil},
|
||||||
{"not absolute", "etc", nil, AbsoluteError("etc")},
|
{"not absolute", "etc", nil, &AbsoluteError{"etc"}},
|
||||||
{"zero", "", nil, AbsoluteError("")},
|
{"zero", "", nil, &AbsoluteError{""}},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
got, err := NewAbs(tc.pathname)
|
got, err := NewAbs(tc.pathname)
|
||||||
if !reflect.DeepEqual(got, tc.want) {
|
if !reflect.DeepEqual(got, tc.want) {
|
||||||
t.Errorf("NewAbs: %#v, want %#v", got, tc.want)
|
t.Errorf("NewAbs: %#v, want %#v", got, tc.want)
|
||||||
@@ -81,12 +65,10 @@ func TestNewAbs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Run("must", func(t *testing.T) {
|
t.Run("must", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
wantPanic := AbsoluteError("etc")
|
wantPanic := `path "etc" is not absolute`
|
||||||
|
|
||||||
if r := recover(); !reflect.DeepEqual(r, wantPanic) {
|
if r := recover(); r != wantPanic {
|
||||||
t.Errorf("MustAbs: panic = %v; want %v", r, wantPanic)
|
t.Errorf("MustAbs: panic = %v; want %v", r, wantPanic)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -97,17 +79,13 @@ func TestNewAbs(t *testing.T) {
|
|||||||
|
|
||||||
func TestAbsoluteString(t *testing.T) {
|
func TestAbsoluteString(t *testing.T) {
|
||||||
t.Run("passthrough", func(t *testing.T) {
|
t.Run("passthrough", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
pathname := "/etc"
|
pathname := "/etc"
|
||||||
if got := unsafeAbs(pathname).String(); got != pathname {
|
if got := (&Absolute{pathname}).String(); got != pathname {
|
||||||
t.Errorf("String: %q, want %q", got, pathname)
|
t.Errorf("String: %q, want %q", got, pathname)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("zero", func(t *testing.T) {
|
t.Run("zero", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
wantPanic := "attempted use of zero Absolute"
|
wantPanic := "attempted use of zero Absolute"
|
||||||
|
|
||||||
@@ -121,8 +99,6 @@ func TestAbsoluteString(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAbsoluteIs(t *testing.T) {
|
func TestAbsoluteIs(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
a, v *Absolute
|
a, v *Absolute
|
||||||
@@ -138,8 +114,6 @@ func TestAbsoluteIs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
if got := tc.a.Is(tc.v); got != tc.want {
|
if got := tc.a.Is(tc.v); got != tc.want {
|
||||||
t.Errorf("Is: %v, want %v", got, tc.want)
|
t.Errorf("Is: %v, want %v", got, tc.want)
|
||||||
}
|
}
|
||||||
@@ -149,12 +123,10 @@ func TestAbsoluteIs(t *testing.T) {
|
|||||||
|
|
||||||
type sCheck struct {
|
type sCheck struct {
|
||||||
Pathname *Absolute `json:"val"`
|
Pathname *Absolute `json:"val"`
|
||||||
Magic uint64 `json:"magic"`
|
Magic int `json:"magic"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCodecAbsolute(t *testing.T) {
|
func TestCodecAbsolute(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
a *Absolute
|
a *Absolute
|
||||||
@@ -170,37 +142,32 @@ func TestCodecAbsolute(t *testing.T) {
|
|||||||
|
|
||||||
{"good", MustAbs("/etc"),
|
{"good", MustAbs("/etc"),
|
||||||
nil,
|
nil,
|
||||||
"\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\b\xff\x80\x00\x04/etc",
|
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\b\xff\x80\x00\x04/etc",
|
||||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x04/etc\x01\xfc\xc0\xed\x00\x00\x00",
|
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x04\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x10\xff\x84\x01\x04/etc\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||||
|
|
||||||
`"/etc"`, `{"val":"/etc","magic":3236757504}`},
|
`"/etc"`, `{"val":"/etc","magic":3236757504}`},
|
||||||
{"not absolute", nil,
|
{"not absolute", nil,
|
||||||
AbsoluteError("etc"),
|
&AbsoluteError{"etc"},
|
||||||
"\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc",
|
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc",
|
||||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",
|
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x04\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||||
|
|
||||||
`"etc"`, `{"val":"etc","magic":3236757504}`},
|
`"etc"`, `{"val":"etc","magic":3236757504}`},
|
||||||
{"zero", nil,
|
{"zero", nil,
|
||||||
new(AbsoluteError),
|
new(AbsoluteError),
|
||||||
"\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x04\xff\x80\x00\x00",
|
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x04\xff\x80\x00\x00",
|
||||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\f\xff\x84\x01\x00\x01\xfb\x01\x81\xda\x00\x00\x00",
|
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x04\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\f\xff\x84\x01\x00\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||||
`""`, `{"val":"","magic":3236757504}`},
|
`""`, `{"val":"","magic":3236757504}`},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("gob", func(t *testing.T) {
|
t.Run("gob", func(t *testing.T) {
|
||||||
if tc.gob == "\x00" && tc.sGob == "\x00" {
|
if tc.gob == "\x00" && tc.sGob == "\x00" {
|
||||||
// these values mark the current test to skip gob
|
// these values mark the current test to skip gob
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("encode", func(t *testing.T) {
|
t.Run("encode", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// encode is unchecked
|
// encode is unchecked
|
||||||
if errors.Is(tc.wantErr, syscall.EINVAL) {
|
if errors.Is(tc.wantErr, syscall.EINVAL) {
|
||||||
return
|
return
|
||||||
@@ -237,8 +204,6 @@ func TestCodecAbsolute(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("decode", func(t *testing.T) {
|
t.Run("decode", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
{
|
{
|
||||||
var gotA *Absolute
|
var gotA *Absolute
|
||||||
err := gob.NewDecoder(strings.NewReader(tc.gob)).Decode(&gotA)
|
err := gob.NewDecoder(strings.NewReader(tc.gob)).Decode(&gotA)
|
||||||
@@ -273,11 +238,7 @@ func TestCodecAbsolute(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("json", func(t *testing.T) {
|
t.Run("json", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("marshal", func(t *testing.T) {
|
t.Run("marshal", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// marshal is unchecked
|
// marshal is unchecked
|
||||||
if errors.Is(tc.wantErr, syscall.EINVAL) {
|
if errors.Is(tc.wantErr, syscall.EINVAL) {
|
||||||
return
|
return
|
||||||
@@ -312,8 +273,6 @@ func TestCodecAbsolute(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unmarshal", func(t *testing.T) {
|
t.Run("unmarshal", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
{
|
{
|
||||||
var gotA *Absolute
|
var gotA *Absolute
|
||||||
err := json.Unmarshal([]byte(tc.json), &gotA)
|
err := json.Unmarshal([]byte(tc.json), &gotA)
|
||||||
@@ -347,14 +306,17 @@ func TestCodecAbsolute(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.Run("json passthrough", func(t *testing.T) {
|
||||||
|
wantErr := "invalid character ':' looking for beginning of value"
|
||||||
|
if err := new(Absolute).UnmarshalJSON([]byte(":3")); err == nil || err.Error() != wantErr {
|
||||||
|
t.Errorf("UnmarshalJSON: error = %v, want %s", err, wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAbsoluteWrap(t *testing.T) {
|
func TestAbsoluteWrap(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("join", func(t *testing.T) {
|
t.Run("join", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
want := "/etc/nix/nix.conf"
|
want := "/etc/nix/nix.conf"
|
||||||
if got := MustAbs("/etc").Append("nix", "nix.conf"); got.String() != want {
|
if got := MustAbs("/etc").Append("nix", "nix.conf"); got.String() != want {
|
||||||
t.Errorf("Append: %q, want %q", got, want)
|
t.Errorf("Append: %q, want %q", got, want)
|
||||||
@@ -362,8 +324,6 @@ func TestAbsoluteWrap(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("dir", func(t *testing.T) {
|
t.Run("dir", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
want := "/"
|
want := "/"
|
||||||
if got := MustAbs("/etc").Dir(); got.String() != want {
|
if got := MustAbs("/etc").Dir(); got.String() != want {
|
||||||
t.Errorf("Dir: %q, want %q", got, want)
|
t.Errorf("Dir: %q, want %q", got, want)
|
||||||
@@ -371,8 +331,6 @@ func TestAbsoluteWrap(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("sort", func(t *testing.T) {
|
t.Run("sort", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")}
|
want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")}
|
||||||
got := []*Absolute{MustAbs("/proc"), MustAbs("/sys"), MustAbs("/etc")}
|
got := []*Absolute{MustAbs("/proc"), MustAbs("/sys"), MustAbs("/etc")}
|
||||||
SortAbs(got)
|
SortAbs(got)
|
||||||
@@ -382,8 +340,6 @@ func TestAbsoluteWrap(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("compact", func(t *testing.T) {
|
t.Run("compact", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")}
|
want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")}
|
||||||
if got := CompactAbs([]*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/proc"), MustAbs("/sys")}); !reflect.DeepEqual(got, want) {
|
if got := CompactAbs([]*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/proc"), MustAbs("/sys")}); !reflect.DeepEqual(got, want) {
|
||||||
t.Errorf("CompactAbs: %#v, want %#v", got, want)
|
t.Errorf("CompactAbs: %#v, want %#v", got, want)
|
||||||
+10
-16
@@ -3,25 +3,20 @@ package container
|
|||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
"hakurei.app/fhs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() { gob.Register(new(AutoEtcOp)) }
|
func init() { gob.Register(new(AutoEtcOp)) }
|
||||||
|
|
||||||
// Etc is a helper for appending [AutoEtcOp] to [Ops].
|
// Etc appends an [Op] that expands host /etc into a toplevel symlink mirror with /etc semantics.
|
||||||
func (f *Ops) Etc(host *check.Absolute, prefix string) *Ops {
|
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
|
||||||
|
func (f *Ops) Etc(host *Absolute, prefix string) *Ops {
|
||||||
e := &AutoEtcOp{prefix}
|
e := &AutoEtcOp{prefix}
|
||||||
f.Mkdir(fhs.AbsEtc, 0755)
|
f.Mkdir(AbsFHSEtc, 0755)
|
||||||
f.Bind(host, e.hostPath(), 0)
|
f.Bind(host, e.hostPath(), 0)
|
||||||
*f = append(*f, e)
|
*f = append(*f, e)
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
// AutoEtcOp expands host /etc into a toplevel symlink mirror with /etc semantics.
|
|
||||||
//
|
|
||||||
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
|
|
||||||
type AutoEtcOp struct{ Prefix string }
|
type AutoEtcOp struct{ Prefix string }
|
||||||
|
|
||||||
func (e *AutoEtcOp) Valid() bool { return e != nil }
|
func (e *AutoEtcOp) Valid() bool { return e != nil }
|
||||||
@@ -32,7 +27,7 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
}
|
}
|
||||||
state.nonrepeatable |= nrAutoEtc
|
state.nonrepeatable |= nrAutoEtc
|
||||||
|
|
||||||
const target = sysrootPath + fhs.Etc
|
const target = sysrootPath + FHSEtc
|
||||||
rel := e.hostRel() + "/"
|
rel := e.hostRel() + "/"
|
||||||
|
|
||||||
if err := k.mkdirAll(target, 0755); err != nil {
|
if err := k.mkdirAll(target, 0755); err != nil {
|
||||||
@@ -47,7 +42,7 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
case ".host", "passwd", "group":
|
case ".host", "passwd", "group":
|
||||||
|
|
||||||
case "mtab":
|
case "mtab":
|
||||||
if err = k.symlink(fhs.Proc+"mounts", target+n); err != nil {
|
if err = k.symlink(FHSProc+"mounts", target+n); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,14 +56,13 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (e *AutoEtcOp) late(*setupState, syscallDispatcher) error { return nil }
|
|
||||||
|
|
||||||
func (e *AutoEtcOp) hostPath() *check.Absolute { return fhs.AbsEtc.Append(e.hostRel()) }
|
func (e *AutoEtcOp) hostPath() *Absolute { return AbsFHSEtc.Append(e.hostRel()) }
|
||||||
func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
|
func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
|
||||||
|
|
||||||
func (e *AutoEtcOp) Is(op Op) bool {
|
func (e *AutoEtcOp) Is(op Op) bool {
|
||||||
ve, ok := op.(*AutoEtcOp)
|
ve, ok := op.(*AutoEtcOp)
|
||||||
return ok && e.Valid() && ve.Valid() && *e == *ve
|
return ok && e.Valid() && ve.Valid() && *e == *ve
|
||||||
}
|
}
|
||||||
func (*AutoEtcOp) prefix() (string, bool) { return "setting up", true }
|
func (*AutoEtcOp) prefix() string { return "setting up" }
|
||||||
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }
|
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }
|
||||||
|
|||||||
@@ -5,15 +5,11 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"hakurei.app/check"
|
"hakurei.app/container/stub"
|
||||||
"hakurei.app/internal/stub"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAutoEtcOp(t *testing.T) {
|
func TestAutoEtcOp(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("nonrepeatable", func(t *testing.T) {
|
t.Run("nonrepeatable", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
wantErr := OpRepeatError("autoetc")
|
wantErr := OpRepeatError("autoetc")
|
||||||
if err := (&AutoEtcOp{Prefix: "81ceabb30d37bbdb3868004629cb84e9"}).apply(&setupState{nonrepeatable: nrAutoEtc}, nil); !errors.Is(err, wantErr) {
|
if err := (&AutoEtcOp{Prefix: "81ceabb30d37bbdb3868004629cb84e9"}).apply(&setupState{nonrepeatable: nrAutoEtc}, nil); !errors.Is(err, wantErr) {
|
||||||
t.Errorf("apply: error = %v, want %v", err, wantErr)
|
t.Errorf("apply: error = %v, want %v", err, wantErr)
|
||||||
@@ -260,11 +256,11 @@ func TestAutoEtcOp(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
checkOpsBuilder(t, []opsBuilderTestCase{
|
checkOpsBuilder(t, []opsBuilderTestCase{
|
||||||
{"pd", new(Ops).Etc(check.MustAbs("/etc/"), "048090b6ed8f9ebb10e275ff5d8c0659"), Ops{
|
{"pd", new(Ops).Etc(MustAbs("/etc/"), "048090b6ed8f9ebb10e275ff5d8c0659"), Ops{
|
||||||
&MkdirOp{Path: check.MustAbs("/etc/"), Perm: 0755},
|
&MkdirOp{Path: MustAbs("/etc/"), Perm: 0755},
|
||||||
&BindMountOp{
|
&BindMountOp{
|
||||||
Source: check.MustAbs("/etc/"),
|
Source: MustAbs("/etc/"),
|
||||||
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||||
},
|
},
|
||||||
&AutoEtcOp{Prefix: "048090b6ed8f9ebb10e275ff5d8c0659"},
|
&AutoEtcOp{Prefix: "048090b6ed8f9ebb10e275ff5d8c0659"},
|
||||||
}},
|
}},
|
||||||
@@ -283,7 +279,6 @@ func TestAutoEtcOp(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("host path rel", func(t *testing.T) {
|
t.Run("host path rel", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
op := &AutoEtcOp{Prefix: "048090b6ed8f9ebb10e275ff5d8c0659"}
|
op := &AutoEtcOp{Prefix: "048090b6ed8f9ebb10e275ff5d8c0659"}
|
||||||
wantHostPath := "/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"
|
wantHostPath := "/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"
|
||||||
wantHostRel := ".host/048090b6ed8f9ebb10e275ff5d8c0659"
|
wantHostRel := ".host/048090b6ed8f9ebb10e275ff5d8c0659"
|
||||||
|
|||||||
+11
-19
@@ -3,32 +3,26 @@ package container
|
|||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
"hakurei.app/fhs"
|
|
||||||
"hakurei.app/message"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() { gob.Register(new(AutoRootOp)) }
|
func init() { gob.Register(new(AutoRootOp)) }
|
||||||
|
|
||||||
// Root is a helper for appending [AutoRootOp] to [Ops].
|
// Root appends an [Op] that expands a directory into a toplevel bind mount mirror on container root.
|
||||||
func (f *Ops) Root(host *check.Absolute, flags int) *Ops {
|
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
|
||||||
|
func (f *Ops) Root(host *Absolute, flags int) *Ops {
|
||||||
*f = append(*f, &AutoRootOp{host, flags, nil})
|
*f = append(*f, &AutoRootOp{host, flags, nil})
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
// AutoRootOp expands a directory into a toplevel bind mount mirror on container root.
|
|
||||||
//
|
|
||||||
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
|
|
||||||
type AutoRootOp struct {
|
type AutoRootOp struct {
|
||||||
Host *check.Absolute
|
Host *Absolute
|
||||||
// passed through to bindMount
|
// passed through to bindMount
|
||||||
Flags int
|
Flags int
|
||||||
|
|
||||||
// obtained during early;
|
// obtained during early;
|
||||||
// these wrap the underlying Op because BindMountOp is relatively complex,
|
// these wrap the underlying Op because BindMountOp is relatively complex,
|
||||||
// so duplicating that code would be unwise
|
// so duplicating that code would be unwise
|
||||||
resolved []*BindMountOp
|
resolved []Op
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *AutoRootOp) Valid() bool { return r != nil && r.Host != nil }
|
func (r *AutoRootOp) Valid() bool { return r != nil && r.Host != nil }
|
||||||
@@ -37,14 +31,13 @@ func (r *AutoRootOp) early(state *setupState, k syscallDispatcher) error {
|
|||||||
if d, err := k.readdir(r.Host.String()); err != nil {
|
if d, err := k.readdir(r.Host.String()); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
r.resolved = make([]*BindMountOp, 0, len(d))
|
r.resolved = make([]Op, 0, len(d))
|
||||||
for _, ent := range d {
|
for _, ent := range d {
|
||||||
name := ent.Name()
|
name := ent.Name()
|
||||||
if IsAutoRootBindable(state, name) {
|
if IsAutoRootBindable(name) {
|
||||||
// careful: the Valid method is skipped, make sure this is always valid
|
|
||||||
op := &BindMountOp{
|
op := &BindMountOp{
|
||||||
Source: r.Host.Append(name),
|
Source: r.Host.Append(name),
|
||||||
Target: fhs.AbsRoot.Append(name),
|
Target: AbsFHSRoot.Append(name),
|
||||||
Flags: r.Flags,
|
Flags: r.Flags,
|
||||||
}
|
}
|
||||||
if err = op.early(state, k); err != nil {
|
if err = op.early(state, k); err != nil {
|
||||||
@@ -64,14 +57,13 @@ func (r *AutoRootOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
state.nonrepeatable |= nrAutoRoot
|
state.nonrepeatable |= nrAutoRoot
|
||||||
|
|
||||||
for _, op := range r.resolved {
|
for _, op := range r.resolved {
|
||||||
// these are exclusively BindMountOp, do not attempt to print identifying message
|
k.verbosef("%s %s", op.prefix(), op)
|
||||||
if err := op.apply(state, k); err != nil {
|
if err := op.apply(state, k); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (r *AutoRootOp) late(*setupState, syscallDispatcher) error { return nil }
|
|
||||||
|
|
||||||
func (r *AutoRootOp) Is(op Op) bool {
|
func (r *AutoRootOp) Is(op Op) bool {
|
||||||
vr, ok := op.(*AutoRootOp)
|
vr, ok := op.(*AutoRootOp)
|
||||||
@@ -79,13 +71,13 @@ func (r *AutoRootOp) Is(op Op) bool {
|
|||||||
r.Host.Is(vr.Host) &&
|
r.Host.Is(vr.Host) &&
|
||||||
r.Flags == vr.Flags
|
r.Flags == vr.Flags
|
||||||
}
|
}
|
||||||
func (*AutoRootOp) prefix() (string, bool) { return "setting up", true }
|
func (*AutoRootOp) prefix() string { return "setting up" }
|
||||||
func (r *AutoRootOp) String() string {
|
func (r *AutoRootOp) String() string {
|
||||||
return fmt.Sprintf("auto root %q flags %#x", r.Host, r.Flags)
|
return fmt.Sprintf("auto root %q flags %#x", r.Host, r.Flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAutoRootBindable returns whether a dir entry name is selected for AutoRoot.
|
// IsAutoRootBindable returns whether a dir entry name is selected for AutoRoot.
|
||||||
func IsAutoRootBindable(msg message.Msg, name string) bool {
|
func IsAutoRootBindable(name string) bool {
|
||||||
switch name {
|
switch name {
|
||||||
case "proc", "dev", "tmp", "mnt", "etc":
|
case "proc", "dev", "tmp", "mnt", "etc":
|
||||||
|
|
||||||
|
|||||||
+64
-77
@@ -5,15 +5,11 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"hakurei.app/check"
|
"hakurei.app/container/stub"
|
||||||
"hakurei.app/container/std"
|
|
||||||
"hakurei.app/internal/stub"
|
|
||||||
"hakurei.app/message"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAutoRootOp(t *testing.T) {
|
func TestAutoRootOp(t *testing.T) {
|
||||||
t.Run("nonrepeatable", func(t *testing.T) {
|
t.Run("nonrepeatable", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
wantErr := OpRepeatError("autoroot")
|
wantErr := OpRepeatError("autoroot")
|
||||||
if err := new(AutoRootOp).apply(&setupState{nonrepeatable: nrAutoRoot}, nil); !errors.Is(err, wantErr) {
|
if err := new(AutoRootOp).apply(&setupState{nonrepeatable: nrAutoRoot}, nil); !errors.Is(err, wantErr) {
|
||||||
t.Errorf("apply: error = %v, want %v", err, wantErr)
|
t.Errorf("apply: error = %v, want %v", err, wantErr)
|
||||||
@@ -22,15 +18,15 @@ func TestAutoRootOp(t *testing.T) {
|
|||||||
|
|
||||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||||
{"readdir", &Params{ParentPerm: 0750}, &AutoRootOp{
|
{"readdir", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||||
Host: check.MustAbs("/"),
|
Host: MustAbs("/"),
|
||||||
Flags: std.BindWritable,
|
Flags: BindWritable,
|
||||||
}, []stub.Call{
|
}, []stub.Call{
|
||||||
call("readdir", stub.ExpectArgs{"/"}, stubDir(), stub.UniqueError(2)),
|
call("readdir", stub.ExpectArgs{"/"}, stubDir(), stub.UniqueError(2)),
|
||||||
}, stub.UniqueError(2), nil, nil},
|
}, stub.UniqueError(2), nil, nil},
|
||||||
|
|
||||||
{"early", &Params{ParentPerm: 0750}, &AutoRootOp{
|
{"early", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||||
Host: check.MustAbs("/"),
|
Host: MustAbs("/"),
|
||||||
Flags: std.BindWritable,
|
Flags: BindWritable,
|
||||||
}, []stub.Call{
|
}, []stub.Call{
|
||||||
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||||
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
|
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
|
||||||
@@ -38,8 +34,8 @@ func TestAutoRootOp(t *testing.T) {
|
|||||||
}, stub.UniqueError(1), nil, nil},
|
}, stub.UniqueError(1), nil, nil},
|
||||||
|
|
||||||
{"apply", &Params{ParentPerm: 0750}, &AutoRootOp{
|
{"apply", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||||
Host: check.MustAbs("/"),
|
Host: MustAbs("/"),
|
||||||
Flags: std.BindWritable,
|
Flags: BindWritable,
|
||||||
}, []stub.Call{
|
}, []stub.Call{
|
||||||
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||||
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
|
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
|
||||||
@@ -55,12 +51,13 @@ func TestAutoRootOp(t *testing.T) {
|
|||||||
call("evalSymlinks", stub.ExpectArgs{"/usr"}, "/usr", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/usr"}, "/usr", nil),
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/var"}, "/var", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/var"}, "/var", nil),
|
||||||
}, nil, []stub.Call{
|
}, nil, []stub.Call{
|
||||||
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(false), stub.UniqueError(0)),
|
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(false), stub.UniqueError(0)),
|
||||||
}, stub.UniqueError(0)},
|
}, stub.UniqueError(0)},
|
||||||
|
|
||||||
{"success pd", &Params{ParentPerm: 0750}, &AutoRootOp{
|
{"success pd", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||||
Host: check.MustAbs("/"),
|
Host: MustAbs("/"),
|
||||||
Flags: std.BindWritable,
|
Flags: BindWritable,
|
||||||
}, []stub.Call{
|
}, []stub.Call{
|
||||||
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||||
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
|
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
|
||||||
@@ -76,21 +73,21 @@ func TestAutoRootOp(t *testing.T) {
|
|||||||
call("evalSymlinks", stub.ExpectArgs{"/usr"}, "/usr", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/usr"}, "/usr", nil),
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/var"}, "/var", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/var"}, "/var", nil),
|
||||||
}, nil, []stub.Call{
|
}, nil, []stub.Call{
|
||||||
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004), false}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/home"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/home", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/home", "/sysroot/home", uintptr(0x4004), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/home"), MustAbs("/home"), MustAbs("/home"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/home"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/home", "/sysroot/home", uintptr(0x4004), false}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/lib64"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/lib64", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/lib64", "/sysroot/lib64", uintptr(0x4004), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/lib64"), MustAbs("/lib64"), MustAbs("/lib64"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/lib64"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/lib64", "/sysroot/lib64", uintptr(0x4004), false}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/lost+found"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/lost+found", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/lost+found", "/sysroot/lost+found", uintptr(0x4004), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/lost+found"), MustAbs("/lost+found"), MustAbs("/lost+found"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/lost+found"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/lost+found", "/sysroot/lost+found", uintptr(0x4004), false}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/nix"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/nix", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", uintptr(0x4004), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/nix"), MustAbs("/nix"), MustAbs("/nix"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/nix"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", uintptr(0x4004), false}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/root"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/root", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/root", "/sysroot/root", uintptr(0x4004), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/root"), MustAbs("/root"), MustAbs("/root"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/root"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/root", "/sysroot/root", uintptr(0x4004), false}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/run"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/run", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/run", "/sysroot/run", uintptr(0x4004), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/run"), MustAbs("/run"), MustAbs("/run"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/run"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/run", "/sysroot/run", uintptr(0x4004), false}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/srv"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/srv", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/srv", "/sysroot/srv", uintptr(0x4004), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/srv"), MustAbs("/srv"), MustAbs("/srv"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/srv"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/srv", "/sysroot/srv", uintptr(0x4004), false}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/sys"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/sys", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/sys", "/sysroot/sys", uintptr(0x4004), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/sys"), MustAbs("/sys"), MustAbs("/sys"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/sys"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/sys", "/sysroot/sys", uintptr(0x4004), false}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/usr"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/usr", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/usr", "/sysroot/usr", uintptr(0x4004), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr"), MustAbs("/usr"), MustAbs("/usr"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/usr"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/usr", "/sysroot/usr", uintptr(0x4004), false}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/var"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/var", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var", "/sysroot/var", uintptr(0x4004), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var"), MustAbs("/var"), MustAbs("/var"), BindWritable}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/var"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var", "/sysroot/var", uintptr(0x4004), false}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
|
|
||||||
{"success", &Params{ParentPerm: 0750}, &AutoRootOp{
|
{"success", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||||
Host: check.MustAbs("/var/lib/planterette/base/debian:f92c9052"),
|
Host: MustAbs("/var/lib/planterette/base/debian:f92c9052"),
|
||||||
}, []stub.Call{
|
}, []stub.Call{
|
||||||
call("readdir", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
call("readdir", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||||
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
|
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
|
||||||
@@ -106,31 +103,31 @@ func TestAutoRootOp(t *testing.T) {
|
|||||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/usr"}, "/var/lib/planterette/base/debian:f92c9052/usr", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/usr"}, "/var/lib/planterette/base/debian:f92c9052/usr", nil),
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/var"}, "/var/lib/planterette/base/debian:f92c9052/var", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/var"}, "/var/lib/planterette/base/debian:f92c9052/var", nil),
|
||||||
}, nil, []stub.Call{
|
}, nil, []stub.Call{
|
||||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/usr/bin"), MustAbs("/var/lib/planterette/base/debian:f92c9052/bin"), MustAbs("/bin"), 0}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/home"), MustAbs("/var/lib/planterette/base/debian:f92c9052/home"), MustAbs("/home"), 0}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005), false}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/lib64"), MustAbs("/var/lib/planterette/base/debian:f92c9052/lib64"), MustAbs("/lib64"), 0}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005), false}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/lost+found"), MustAbs("/var/lib/planterette/base/debian:f92c9052/lost+found"), MustAbs("/lost+found"), 0}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005), false}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/nix"), MustAbs("/var/lib/planterette/base/debian:f92c9052/nix"), MustAbs("/nix"), 0}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005), false}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/root"), MustAbs("/var/lib/planterette/base/debian:f92c9052/root"), MustAbs("/root"), 0}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005), false}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/run"), MustAbs("/var/lib/planterette/base/debian:f92c9052/run"), MustAbs("/run"), 0}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005), false}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/srv"), MustAbs("/var/lib/planterette/base/debian:f92c9052/srv"), MustAbs("/srv"), 0}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005), false}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/sys"), MustAbs("/var/lib/planterette/base/debian:f92c9052/sys"), MustAbs("/sys"), 0}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005), false}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/usr"), MustAbs("/var/lib/planterette/base/debian:f92c9052/usr"), MustAbs("/usr"), 0}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005), false}, nil, nil),
|
||||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/var", "/sysroot/var", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var", "/sysroot/var", uintptr(0x4005), false}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/var"), MustAbs("/var/lib/planterette/base/debian:f92c9052/var"), MustAbs("/var"), 0}}}, nil, nil), call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var", "/sysroot/var", uintptr(0x4005), false}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
})
|
})
|
||||||
|
|
||||||
checkOpsValid(t, []opValidTestCase{
|
checkOpsValid(t, []opValidTestCase{
|
||||||
{"nil", (*AutoRootOp)(nil), false},
|
{"nil", (*AutoRootOp)(nil), false},
|
||||||
{"zero", new(AutoRootOp), false},
|
{"zero", new(AutoRootOp), false},
|
||||||
{"valid", &AutoRootOp{Host: check.MustAbs("/")}, true},
|
{"valid", &AutoRootOp{Host: MustAbs("/")}, true},
|
||||||
})
|
})
|
||||||
|
|
||||||
checkOpsBuilder(t, []opsBuilderTestCase{
|
checkOpsBuilder(t, []opsBuilderTestCase{
|
||||||
{"pd", new(Ops).Root(check.MustAbs("/"), std.BindWritable), Ops{
|
{"pd", new(Ops).Root(MustAbs("/"), BindWritable), Ops{
|
||||||
&AutoRootOp{
|
&AutoRootOp{
|
||||||
Host: check.MustAbs("/"),
|
Host: MustAbs("/"),
|
||||||
Flags: std.BindWritable,
|
Flags: BindWritable,
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
})
|
})
|
||||||
@@ -139,74 +136,64 @@ func TestAutoRootOp(t *testing.T) {
|
|||||||
{"zero", new(AutoRootOp), new(AutoRootOp), false},
|
{"zero", new(AutoRootOp), new(AutoRootOp), false},
|
||||||
|
|
||||||
{"internal ne", &AutoRootOp{
|
{"internal ne", &AutoRootOp{
|
||||||
Host: check.MustAbs("/"),
|
Host: MustAbs("/"),
|
||||||
Flags: std.BindWritable,
|
Flags: BindWritable,
|
||||||
}, &AutoRootOp{
|
}, &AutoRootOp{
|
||||||
Host: check.MustAbs("/"),
|
Host: MustAbs("/"),
|
||||||
Flags: std.BindWritable,
|
Flags: BindWritable,
|
||||||
resolved: []*BindMountOp{new(BindMountOp)},
|
resolved: []Op{new(BindMountOp)},
|
||||||
}, true},
|
}, true},
|
||||||
|
|
||||||
{"flags differs", &AutoRootOp{
|
{"flags differs", &AutoRootOp{
|
||||||
Host: check.MustAbs("/"),
|
Host: MustAbs("/"),
|
||||||
Flags: std.BindWritable | std.BindDevice,
|
Flags: BindWritable | BindDevice,
|
||||||
}, &AutoRootOp{
|
}, &AutoRootOp{
|
||||||
Host: check.MustAbs("/"),
|
Host: MustAbs("/"),
|
||||||
Flags: std.BindWritable,
|
Flags: BindWritable,
|
||||||
}, false},
|
}, false},
|
||||||
|
|
||||||
{"host differs", &AutoRootOp{
|
{"host differs", &AutoRootOp{
|
||||||
Host: check.MustAbs("/tmp/"),
|
Host: MustAbs("/tmp/"),
|
||||||
Flags: std.BindWritable,
|
Flags: BindWritable,
|
||||||
}, &AutoRootOp{
|
}, &AutoRootOp{
|
||||||
Host: check.MustAbs("/"),
|
Host: MustAbs("/"),
|
||||||
Flags: std.BindWritable,
|
Flags: BindWritable,
|
||||||
}, false},
|
}, false},
|
||||||
|
|
||||||
{"equals", &AutoRootOp{
|
{"equals", &AutoRootOp{
|
||||||
Host: check.MustAbs("/"),
|
Host: MustAbs("/"),
|
||||||
Flags: std.BindWritable,
|
Flags: BindWritable,
|
||||||
}, &AutoRootOp{
|
}, &AutoRootOp{
|
||||||
Host: check.MustAbs("/"),
|
Host: MustAbs("/"),
|
||||||
Flags: std.BindWritable,
|
Flags: BindWritable,
|
||||||
}, true},
|
}, true},
|
||||||
})
|
})
|
||||||
|
|
||||||
checkOpMeta(t, []opMetaTestCase{
|
checkOpMeta(t, []opMetaTestCase{
|
||||||
{"root", &AutoRootOp{
|
{"root", &AutoRootOp{
|
||||||
Host: check.MustAbs("/"),
|
Host: MustAbs("/"),
|
||||||
Flags: std.BindWritable,
|
Flags: BindWritable,
|
||||||
}, "setting up", `auto root "/" flags 0x2`},
|
}, "setting up", `auto root "/" flags 0x2`},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsAutoRootBindable(t *testing.T) {
|
func TestIsAutoRootBindable(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
want bool
|
want bool
|
||||||
log bool
|
|
||||||
}{
|
}{
|
||||||
{"proc", false, false},
|
{"proc", false},
|
||||||
{"dev", false, false},
|
{"dev", false},
|
||||||
{"tmp", false, false},
|
{"tmp", false},
|
||||||
{"mnt", false, false},
|
{"mnt", false},
|
||||||
{"etc", false, false},
|
{"etc", false},
|
||||||
{"", false, true},
|
{"", false},
|
||||||
|
|
||||||
{"var", true, false},
|
{"var", true},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
if got := IsAutoRootBindable(tc.name); got != tc.want {
|
||||||
var msg message.Msg
|
|
||||||
if tc.log {
|
|
||||||
msg = &kstub{nil, nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{
|
|
||||||
call("verbose", stub.ExpectArgs{[]any{"got unexpected root entry"}}, nil, nil),
|
|
||||||
}})}
|
|
||||||
}
|
|
||||||
if got := IsAutoRootBindable(msg, tc.name); got != tc.want {
|
|
||||||
t.Errorf("IsAutoRootBindable: %v, want %v", got, tc.want)
|
t.Errorf("IsAutoRootBindable: %v, want %v", got, tc.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
)
|
|
||||||
|
|
||||||
// escapeBinfmt escapes magic/mask sequences in a [BinfmtEntry].
|
|
||||||
func escapeBinfmt(buf *strings.Builder, s string) string {
|
|
||||||
const lowerhex = "0123456789abcdef"
|
|
||||||
|
|
||||||
buf.Reset()
|
|
||||||
for _, c := range unsafe.Slice(unsafe.StringData(s), len(s)) {
|
|
||||||
switch c {
|
|
||||||
case 0, '\\', ':':
|
|
||||||
buf.WriteString(`\x`)
|
|
||||||
buf.WriteByte(lowerhex[c>>4])
|
|
||||||
buf.WriteByte(lowerhex[c&0xf])
|
|
||||||
|
|
||||||
default:
|
|
||||||
buf.WriteByte(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// BinfmtEntry is an entry to be registered by the init process.
|
|
||||||
type BinfmtEntry struct {
|
|
||||||
// The offset of the magic/mask in the file, counted in bytes.
|
|
||||||
Offset byte
|
|
||||||
// The byte sequence binfmt_misc is matching for.
|
|
||||||
Magic string
|
|
||||||
// An (optional, defaults to all 0xff) mask.
|
|
||||||
Mask string
|
|
||||||
// The program that should be invoked with the binary as first argument.
|
|
||||||
Interpreter *check.Absolute
|
|
||||||
}
|
|
||||||
|
|
||||||
// Valid returns whether e can be registered into the kernel.
|
|
||||||
func (e *BinfmtEntry) Valid() bool {
|
|
||||||
return e != nil &&
|
|
||||||
int(e.Offset)+max(len(e.Magic), len(e.Mask)) < 128 &&
|
|
||||||
e.Interpreter != nil && len(e.Interpreter.String()) < 128
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"hakurei.app/fhs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEscapeBinfmt(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
magic string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{"packed DOS applications", "\x0eDEX", "\x0eDEX"},
|
|
||||||
|
|
||||||
{"riscv64 magic",
|
|
||||||
"\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xf3\x00",
|
|
||||||
"\x7fELF\x02\x01\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\x02\\x00\xf3\\x00"},
|
|
||||||
{"riscv64 mask",
|
|
||||||
"\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff",
|
|
||||||
"\xff\xff\xff\xff\xff\xff\xff\\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff"},
|
|
||||||
}
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
got := escapeBinfmt(new(strings.Builder), tc.magic)
|
|
||||||
if got != tc.want {
|
|
||||||
t.Errorf("escapeBinfmt: %q, want %q", got, tc.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBinfmtEntry(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
e BinfmtEntry
|
|
||||||
valid bool
|
|
||||||
}{
|
|
||||||
{"zero", BinfmtEntry{}, false},
|
|
||||||
{"large offset", BinfmtEntry{Offset: 128}, false},
|
|
||||||
{"long magic", BinfmtEntry{Magic: strings.Repeat("\x00", 128)}, false},
|
|
||||||
{"long mask", BinfmtEntry{Mask: strings.Repeat("\x00", 128)}, false},
|
|
||||||
{"valid", BinfmtEntry{Interpreter: fhs.AbsRoot}, true},
|
|
||||||
}
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
if tc.e.Valid() != tc.valid {
|
|
||||||
t.Errorf("Valid: %v", !tc.valid)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+28
-7
@@ -3,8 +3,6 @@ package container
|
|||||||
import (
|
import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"hakurei.app/ext"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -16,9 +14,7 @@ const (
|
|||||||
|
|
||||||
CAP_SYS_ADMIN = 0x15
|
CAP_SYS_ADMIN = 0x15
|
||||||
CAP_SETPCAP = 0x8
|
CAP_SETPCAP = 0x8
|
||||||
CAP_NET_ADMIN = 0xc
|
|
||||||
CAP_DAC_OVERRIDE = 0x1
|
CAP_DAC_OVERRIDE = 0x1
|
||||||
CAP_SETFCAP = 0x1f
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -54,15 +50,40 @@ func capset(hdrp *capHeader, datap *[2]capData) error {
|
|||||||
|
|
||||||
// capBoundingSetDrop drops a capability from the calling thread's capability bounding set.
|
// capBoundingSetDrop drops a capability from the calling thread's capability bounding set.
|
||||||
func capBoundingSetDrop(cap uintptr) error {
|
func capBoundingSetDrop(cap uintptr) error {
|
||||||
return ext.Prctl(syscall.PR_CAPBSET_DROP, cap, 0)
|
r, _, errno := syscall.Syscall(
|
||||||
|
syscall.SYS_PRCTL,
|
||||||
|
syscall.PR_CAPBSET_DROP,
|
||||||
|
cap, 0,
|
||||||
|
)
|
||||||
|
if r != 0 {
|
||||||
|
return errno
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// capAmbientClearAll clears the ambient capability set of the calling thread.
|
// capAmbientClearAll clears the ambient capability set of the calling thread.
|
||||||
func capAmbientClearAll() error {
|
func capAmbientClearAll() error {
|
||||||
return ext.Prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0)
|
r, _, errno := syscall.Syscall(
|
||||||
|
syscall.SYS_PRCTL,
|
||||||
|
PR_CAP_AMBIENT,
|
||||||
|
PR_CAP_AMBIENT_CLEAR_ALL, 0,
|
||||||
|
)
|
||||||
|
if r != 0 {
|
||||||
|
return errno
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// capAmbientRaise adds to the ambient capability set of the calling thread.
|
// capAmbientRaise adds to the ambient capability set of the calling thread.
|
||||||
func capAmbientRaise(cap uintptr) error {
|
func capAmbientRaise(cap uintptr) error {
|
||||||
return ext.Prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap)
|
r, _, errno := syscall.Syscall(
|
||||||
|
syscall.SYS_PRCTL,
|
||||||
|
PR_CAP_AMBIENT,
|
||||||
|
PR_CAP_AMBIENT_RAISE,
|
||||||
|
cap,
|
||||||
|
)
|
||||||
|
if r != 0 {
|
||||||
|
return errno
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package container
|
|||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestCapToIndex(t *testing.T) {
|
func TestCapToIndex(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
cap uintptr
|
cap uintptr
|
||||||
@@ -16,7 +14,6 @@ func TestCapToIndex(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
if got := capToIndex(tc.cap); got != tc.want {
|
if got := capToIndex(tc.cap); got != tc.want {
|
||||||
t.Errorf("capToIndex: %#x, want %#x", got, tc.want)
|
t.Errorf("capToIndex: %#x, want %#x", got, tc.want)
|
||||||
}
|
}
|
||||||
@@ -25,8 +22,6 @@ func TestCapToIndex(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCapToMask(t *testing.T) {
|
func TestCapToMask(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
cap uintptr
|
cap uintptr
|
||||||
@@ -38,7 +33,6 @@ func TestCapToMask(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
if got := capToMask(tc.cap); got != tc.want {
|
if got := capToMask(tc.cap); got != tc.want {
|
||||||
t.Errorf("capToMask: %#x, want %#x", got, tc.want)
|
t.Errorf("capToMask: %#x, want %#x", got, tc.want)
|
||||||
}
|
}
|
||||||
|
|||||||
+82
-318
@@ -1,5 +1,4 @@
|
|||||||
// Package container implements unprivileged Linux containers with built-in
|
// Package container implements unprivileged Linux containers with built-in support for syscall filtering.
|
||||||
// support for syscall filtering.
|
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -12,108 +11,77 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
|
||||||
. "syscall"
|
. "syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
"hakurei.app/container/seccomp"
|
"hakurei.app/container/seccomp"
|
||||||
"hakurei.app/container/std"
|
|
||||||
"hakurei.app/ext"
|
|
||||||
"hakurei.app/fhs"
|
|
||||||
"hakurei.app/internal/landlock"
|
|
||||||
"hakurei.app/message"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// CancelSignal is the signal expected by container init on context cancel.
|
// CancelSignal is the signal expected by container init on context cancel.
|
||||||
// A custom [Container.Cancel] function must eventually deliver this signal.
|
// A custom [Container.Cancel] function must eventually deliver this signal.
|
||||||
CancelSignal = SIGUSR2
|
CancelSignal = SIGTERM
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// Container represents a container environment being prepared or run.
|
// Container represents a container environment being prepared or run.
|
||||||
// None of [Container] methods are safe for concurrent use.
|
// None of [Container] methods are safe for concurrent use.
|
||||||
Container struct {
|
Container struct {
|
||||||
// Whether the container init should stay alive after its parent terminates.
|
|
||||||
AllowOrphan bool
|
|
||||||
// Whether to set SchedPolicy and SchedPriority via sched_setscheduler(2).
|
|
||||||
SetScheduler bool
|
|
||||||
// Scheduling policy to set via sched_setscheduler(2).
|
|
||||||
SchedPolicy ext.SchedPolicy
|
|
||||||
// Scheduling priority to set via sched_setscheduler(2). The zero value
|
|
||||||
// implies the minimum value supported by the current SchedPolicy.
|
|
||||||
SchedPriority ext.Int
|
|
||||||
// Cgroup fd, nil to disable.
|
// Cgroup fd, nil to disable.
|
||||||
Cgroup *int
|
Cgroup *int
|
||||||
// ExtraFiles passed through to initial process in the container, with
|
// ExtraFiles passed through to initial process in the container,
|
||||||
// behaviour identical to its [exec.Cmd] counterpart.
|
// with behaviour identical to its [exec.Cmd] counterpart.
|
||||||
ExtraFiles []*os.File
|
ExtraFiles []*os.File
|
||||||
|
|
||||||
// Write end of a pipe connected to the init to deliver [Params].
|
// param encoder for shim and init
|
||||||
setup [2]*os.File
|
setup *gob.Encoder
|
||||||
// Cancels the context passed to the underlying cmd.
|
// cancels cmd
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
// Closed after Wait returns. Keeps the spawning thread alive.
|
// closed after Wait returns
|
||||||
wait chan struct{}
|
wait chan struct{}
|
||||||
|
|
||||||
Stdin io.Reader
|
Stdin io.Reader
|
||||||
Stdout io.Writer
|
Stdout io.Writer
|
||||||
Stderr io.Writer
|
Stderr io.Writer
|
||||||
|
|
||||||
// Custom cancellation behaviour for the underlying [exec.Cmd]. Must
|
Cancel func(cmd *exec.Cmd) error
|
||||||
// deliver [CancelSignal] before returning.
|
|
||||||
Cancel func(cmd *exec.Cmd) error
|
|
||||||
// Copied to the underlying [exec.Cmd].
|
|
||||||
WaitDelay time.Duration
|
WaitDelay time.Duration
|
||||||
|
|
||||||
// Suppress verbose output of init.
|
|
||||||
Quiet bool
|
|
||||||
|
|
||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
msg message.Msg
|
|
||||||
Params
|
Params
|
||||||
}
|
}
|
||||||
|
|
||||||
// Params holds container configuration and is safe to serialise.
|
// Params holds container configuration and is safe to serialise.
|
||||||
Params struct {
|
Params struct {
|
||||||
// Working directory in the container.
|
// Working directory in the container.
|
||||||
Dir *check.Absolute
|
Dir *Absolute
|
||||||
// Initial process environment.
|
// Initial process environment.
|
||||||
Env []string
|
Env []string
|
||||||
// Pathname of initial process in the container.
|
// Pathname of initial process in the container.
|
||||||
Path *check.Absolute
|
Path *Absolute
|
||||||
// Initial process argv.
|
// Initial process argv.
|
||||||
Args []string
|
Args []string
|
||||||
// Deliver SIGINT to the initial process on context cancellation.
|
// Deliver SIGINT to the initial process on context cancellation.
|
||||||
ForwardCancel bool
|
ForwardCancel bool
|
||||||
// Time to wait for processes lingering after the initial process terminates.
|
// time to wait for linger processes after death of initial process
|
||||||
AdoptWaitDelay time.Duration
|
AdoptWaitDelay time.Duration
|
||||||
|
|
||||||
// Map uid/gid 0 in the init process. Requires [FstypeProc] attached to
|
|
||||||
// [fhs.Proc] in the container filesystem.
|
|
||||||
InitAsRoot bool
|
|
||||||
// Mapped Uid in user namespace.
|
// Mapped Uid in user namespace.
|
||||||
Uid int
|
Uid int
|
||||||
// Mapped Gid in user namespace.
|
// Mapped Gid in user namespace.
|
||||||
Gid int
|
Gid int
|
||||||
// Hostname value in UTS namespace.
|
// Hostname value in UTS namespace.
|
||||||
Hostname string
|
Hostname string
|
||||||
// Register binfmt_misc entries.
|
|
||||||
Binfmt []BinfmtEntry
|
|
||||||
// Alternative pathname to attach binfmt_misc filesystem. The zero value
|
|
||||||
// requires [FstypeProc] to be made available at [fhs.Proc].
|
|
||||||
BinfmtPath *check.Absolute
|
|
||||||
// Sequential container setup ops.
|
// Sequential container setup ops.
|
||||||
*Ops
|
*Ops
|
||||||
|
|
||||||
// Seccomp system call filter rules.
|
// Seccomp system call filter rules.
|
||||||
SeccompRules []std.NativeRule
|
SeccompRules []seccomp.NativeRule
|
||||||
// Extra seccomp flags.
|
// Extra seccomp flags.
|
||||||
SeccompFlags seccomp.ExportFlag
|
SeccompFlags seccomp.ExportFlag
|
||||||
// Seccomp presets. Has no effect unless SeccompRules is zero-length.
|
// Seccomp presets. Has no effect unless SeccompRules is zero-length.
|
||||||
SeccompPresets std.FilterPreset
|
SeccompPresets seccomp.FilterPreset
|
||||||
// Do not load seccomp program.
|
// Do not load seccomp program.
|
||||||
SeccompDisable bool
|
SeccompDisable bool
|
||||||
|
|
||||||
@@ -167,18 +135,11 @@ func (e *StartError) Error() string {
|
|||||||
// Message returns a user-facing error message.
|
// Message returns a user-facing error message.
|
||||||
func (e *StartError) Message() string {
|
func (e *StartError) Message() string {
|
||||||
if e.Passthrough {
|
if e.Passthrough {
|
||||||
var (
|
|
||||||
numError *strconv.NumError
|
|
||||||
)
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case errors.As(e.Err, new(*os.PathError)),
|
case errors.As(e.Err, new(*os.PathError)),
|
||||||
errors.As(e.Err, new(*os.SyscallError)):
|
errors.As(e.Err, new(*os.SyscallError)):
|
||||||
return "cannot " + e.Err.Error()
|
return "cannot " + e.Err.Error()
|
||||||
|
|
||||||
case errors.As(e.Err, &numError) && numError != nil:
|
|
||||||
return "cannot parse " + strconv.Quote(numError.Num) + ": " + numError.Err.Error()
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return e.Err.Error()
|
return e.Err.Error()
|
||||||
}
|
}
|
||||||
@@ -189,59 +150,28 @@ func (e *StartError) Message() string {
|
|||||||
return "cannot " + e.Error()
|
return "cannot " + e.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
// for ensureCloseOnExec
|
|
||||||
var (
|
|
||||||
closeOnExecOnce sync.Once
|
|
||||||
closeOnExecErr error
|
|
||||||
)
|
|
||||||
|
|
||||||
// ensureCloseOnExec ensures all currently open file descriptors have the
|
|
||||||
// syscall.FD_CLOEXEC flag set.
|
|
||||||
//
|
|
||||||
// This is only ran once as it is intended to handle files left open by the
|
|
||||||
// parent, and any file opened on this side should already have
|
|
||||||
// syscall.FD_CLOEXEC set.
|
|
||||||
func ensureCloseOnExec() error {
|
|
||||||
closeOnExecOnce.Do(func() { closeOnExecErr = doCloseOnExec() })
|
|
||||||
|
|
||||||
if closeOnExecErr == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &StartError{
|
|
||||||
Fatal: true,
|
|
||||||
Step: "set FD_CLOEXEC on all open files",
|
|
||||||
Err: closeOnExecErr,
|
|
||||||
Passthrough: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start starts the container init. The init process blocks until Serve is called.
|
// Start starts the container init. The init process blocks until Serve is called.
|
||||||
func (p *Container) Start() error {
|
func (p *Container) Start() error {
|
||||||
if p == nil || p.cmd == nil ||
|
if p.cmd != nil {
|
||||||
p.Ops == nil || len(*p.Ops) == 0 {
|
|
||||||
return errors.New("container: starting an invalid container")
|
|
||||||
}
|
|
||||||
if p.cmd.Process != nil {
|
|
||||||
return errors.New("container: already started")
|
return errors.New("container: already started")
|
||||||
}
|
}
|
||||||
if !p.InitAsRoot && len(p.Binfmt) > 0 {
|
if p.Ops == nil || len(*p.Ops) == 0 {
|
||||||
return errors.New("container: init as root required, but not enabled")
|
return errors.New("container: starting an empty container")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ensureCloseOnExec(); err != nil {
|
ctx, cancel := context.WithCancel(p.ctx)
|
||||||
return err
|
p.cancel = cancel
|
||||||
}
|
|
||||||
|
|
||||||
// map to overflow id to work around ownership checks
|
// map to overflow id to work around ownership checks
|
||||||
if p.Uid < 1 {
|
if p.Uid < 1 {
|
||||||
p.Uid = OverflowUid(p.msg)
|
p.Uid = OverflowUid()
|
||||||
}
|
}
|
||||||
if p.Gid < 1 {
|
if p.Gid < 1 {
|
||||||
p.Gid = OverflowGid(p.msg)
|
p.Gid = OverflowGid()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !p.RetainSession {
|
if !p.RetainSession {
|
||||||
p.SeccompPresets |= std.PresetDenyTTY
|
p.SeccompPresets |= seccomp.PresetDenyTTY
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.AdoptWaitDelay == 0 {
|
if p.AdoptWaitDelay == 0 {
|
||||||
@@ -252,26 +182,19 @@ func (p *Container) Start() error {
|
|||||||
p.AdoptWaitDelay = 0
|
p.AdoptWaitDelay = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.cmd.Stdin == nil {
|
p.cmd = exec.CommandContext(ctx, MustExecutable())
|
||||||
p.cmd.Stdin = p.Stdin
|
|
||||||
}
|
|
||||||
if p.cmd.Stdout == nil {
|
|
||||||
p.cmd.Stdout = p.Stdout
|
|
||||||
}
|
|
||||||
if p.cmd.Stderr == nil {
|
|
||||||
p.cmd.Stderr = p.Stderr
|
|
||||||
}
|
|
||||||
|
|
||||||
p.cmd.Args = []string{initName}
|
p.cmd.Args = []string{initName}
|
||||||
|
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
|
||||||
p.cmd.WaitDelay = p.WaitDelay
|
p.cmd.WaitDelay = p.WaitDelay
|
||||||
if p.Cancel != nil {
|
if p.Cancel != nil {
|
||||||
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
|
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
|
||||||
} else {
|
} else {
|
||||||
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) }
|
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) }
|
||||||
}
|
}
|
||||||
p.cmd.Dir = fhs.Root
|
p.cmd.Dir = FHSRoot
|
||||||
p.cmd.SysProcAttr = &SysProcAttr{
|
p.cmd.SysProcAttr = &SysProcAttr{
|
||||||
Setsid: !p.RetainSession,
|
Setsid: !p.RetainSession,
|
||||||
|
Pdeathsig: SIGKILL,
|
||||||
Cloneflags: CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS |
|
Cloneflags: CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS |
|
||||||
CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP,
|
CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP,
|
||||||
|
|
||||||
@@ -280,47 +203,24 @@ func (p *Container) Start() error {
|
|||||||
CAP_SYS_ADMIN,
|
CAP_SYS_ADMIN,
|
||||||
// drop capabilities
|
// drop capabilities
|
||||||
CAP_SETPCAP,
|
CAP_SETPCAP,
|
||||||
// bring up loopback interface
|
|
||||||
CAP_NET_ADMIN,
|
|
||||||
// overlay access to upperdir and workdir
|
// overlay access to upperdir and workdir
|
||||||
CAP_DAC_OVERRIDE,
|
CAP_DAC_OVERRIDE,
|
||||||
},
|
},
|
||||||
|
|
||||||
UseCgroupFD: p.Cgroup != nil,
|
UseCgroupFD: p.Cgroup != nil,
|
||||||
}
|
}
|
||||||
if !p.AllowOrphan {
|
|
||||||
p.cmd.SysProcAttr.Pdeathsig = SIGKILL
|
|
||||||
}
|
|
||||||
if p.cmd.SysProcAttr.UseCgroupFD {
|
if p.cmd.SysProcAttr.UseCgroupFD {
|
||||||
p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
|
p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
|
||||||
}
|
}
|
||||||
if !p.HostNet {
|
if !p.HostNet {
|
||||||
p.cmd.SysProcAttr.Cloneflags |= CLONE_NEWNET
|
p.cmd.SysProcAttr.Cloneflags |= CLONE_NEWNET
|
||||||
}
|
}
|
||||||
if p.InitAsRoot {
|
|
||||||
p.cmd.SysProcAttr.AmbientCaps = append(p.cmd.SysProcAttr.AmbientCaps,
|
|
||||||
// mappings during init as root
|
|
||||||
CAP_SETFCAP,
|
|
||||||
)
|
|
||||||
|
|
||||||
if !p.SeccompDisable &&
|
|
||||||
len(p.SeccompRules) == 0 &&
|
|
||||||
p.SeccompPresets&std.PresetDenyNS != 0 {
|
|
||||||
return errors.New("container: as root requires late namespace creation")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// place setup pipe before user supplied extra files, this is later restored by init
|
// place setup pipe before user supplied extra files, this is later restored by init
|
||||||
if r, w, err := os.Pipe(); err != nil {
|
if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil {
|
||||||
return &StartError{
|
return &StartError{true, "set up params stream", err, false, false}
|
||||||
Fatal: true,
|
|
||||||
Step: "set up params stream",
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
fd := 3 + len(p.cmd.ExtraFiles)
|
p.setup = e
|
||||||
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, r)
|
|
||||||
p.setup[0], p.setup[1] = r, w
|
|
||||||
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
|
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
|
||||||
}
|
}
|
||||||
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
|
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
|
||||||
@@ -330,63 +230,46 @@ func (p *Container) Start() error {
|
|||||||
runtime.LockOSThread()
|
runtime.LockOSThread()
|
||||||
p.wait = make(chan struct{})
|
p.wait = make(chan struct{})
|
||||||
|
|
||||||
// setup depending on per-thread state must happen here
|
done <- func() error { // setup depending on per-thread state must happen here
|
||||||
done <- func() error {
|
// PR_SET_NO_NEW_PRIVS: depends on per-thread state but acts on all processes created from that thread
|
||||||
// PR_SET_NO_NEW_PRIVS: thread-directed but acts on all processes
|
if err := SetNoNewPrivs(); err != nil {
|
||||||
// created from the calling thread
|
return &StartError{true, "prctl(PR_SET_NO_NEW_PRIVS)", err, false, false}
|
||||||
if err := setNoNewPrivs(); err != nil {
|
|
||||||
return &StartError{
|
|
||||||
Fatal: true,
|
|
||||||
Step: "prctl(PR_SET_NO_NEW_PRIVS)",
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// landlock: depends on per-thread state but acts on a process group
|
// landlock: depends on per-thread state but acts on a process group
|
||||||
{
|
{
|
||||||
rulesetAttr := &landlock.RulesetAttr{
|
rulesetAttr := &RulesetAttr{Scoped: LANDLOCK_SCOPE_SIGNAL}
|
||||||
Scoped: landlock.LANDLOCK_SCOPE_SIGNAL,
|
|
||||||
}
|
|
||||||
if !p.HostAbstract {
|
if !p.HostAbstract {
|
||||||
rulesetAttr.Scoped |= landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
|
rulesetAttr.Scoped |= LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
|
||||||
}
|
}
|
||||||
|
|
||||||
if abi, err := landlock.GetABI(); err != nil {
|
if abi, err := LandlockGetABI(); err != nil {
|
||||||
if p.HostAbstract || !p.HostNet {
|
if p.HostAbstract {
|
||||||
// landlock can be skipped here as it restricts access
|
// landlock can be skipped here as it restricts access to resources
|
||||||
// to resources already covered by namespaces (pid, net)
|
// already covered by namespaces (pid)
|
||||||
goto landlockOut
|
goto landlockOut
|
||||||
}
|
}
|
||||||
return &StartError{Step: "get landlock ABI", Err: err}
|
return &StartError{false, "get landlock ABI", err, false, false}
|
||||||
} else if abi < 6 {
|
} else if abi < 6 {
|
||||||
if p.HostAbstract {
|
if p.HostAbstract {
|
||||||
// see above comment
|
// see above comment
|
||||||
goto landlockOut
|
goto landlockOut
|
||||||
}
|
}
|
||||||
return &StartError{
|
return &StartError{false, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET", ENOSYS, true, false}
|
||||||
Step: "kernel too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
|
} else {
|
||||||
Err: ENOSYS,
|
msg.Verbosef("landlock abi version %d", abi)
|
||||||
Origin: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if rulesetFd, err := rulesetAttr.Create(0); err != nil {
|
if rulesetFd, err := rulesetAttr.Create(0); err != nil {
|
||||||
return &StartError{
|
return &StartError{true, "create landlock ruleset", err, false, false}
|
||||||
Fatal: true,
|
|
||||||
Step: "create landlock ruleset",
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if err = landlock.RestrictSelf(rulesetFd, 0); err != nil {
|
msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
|
||||||
|
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
|
||||||
_ = Close(rulesetFd)
|
_ = Close(rulesetFd)
|
||||||
return &StartError{
|
return &StartError{true, "enforce landlock ruleset", err, false, false}
|
||||||
Fatal: true,
|
|
||||||
Step: "enforce landlock ruleset",
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if err = Close(rulesetFd); err != nil {
|
if err = Close(rulesetFd); err != nil {
|
||||||
p.msg.Verbosef("cannot close landlock ruleset: %v", err)
|
msg.Verbosef("cannot close landlock ruleset: %v", err)
|
||||||
// not fatal
|
// not fatal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,51 +277,9 @@ func (p *Container) Start() error {
|
|||||||
landlockOut:
|
landlockOut:
|
||||||
}
|
}
|
||||||
|
|
||||||
// sched_setscheduler: thread-directed but acts on all processes
|
msg.Verbose("starting container init")
|
||||||
// created from the calling thread
|
|
||||||
if p.SetScheduler {
|
|
||||||
if p.SchedPolicy < 0 || p.SchedPolicy > ext.SCHED_LAST {
|
|
||||||
return &StartError{
|
|
||||||
Fatal: false,
|
|
||||||
Step: "set scheduling policy",
|
|
||||||
Err: EINVAL,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var param schedParam
|
|
||||||
if priority, err := p.SchedPolicy.GetPriorityMin(); err != nil {
|
|
||||||
return &StartError{
|
|
||||||
Fatal: true,
|
|
||||||
Step: "get minimum priority",
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
param.priority = max(priority, p.SchedPriority)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.msg.Verbosef(
|
|
||||||
"setting scheduling policy %s priority %d",
|
|
||||||
p.SchedPolicy, param.priority,
|
|
||||||
)
|
|
||||||
if err := schedSetscheduler(
|
|
||||||
0, // calling thread
|
|
||||||
p.SchedPolicy,
|
|
||||||
¶m,
|
|
||||||
); err != nil {
|
|
||||||
return &StartError{
|
|
||||||
Fatal: true,
|
|
||||||
Step: "set scheduling policy",
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.cmd.Start(); err != nil {
|
if err := p.cmd.Start(); err != nil {
|
||||||
return &StartError{
|
return &StartError{false, "start container init", err, false, true}
|
||||||
Step: "start container init",
|
|
||||||
Err: err,
|
|
||||||
Passthrough: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}()
|
}()
|
||||||
@@ -450,76 +291,46 @@ func (p *Container) Start() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Serve serves [Container.Params] to the container init.
|
// Serve serves [Container.Params] to the container init.
|
||||||
//
|
|
||||||
// Serve must only be called once.
|
// Serve must only be called once.
|
||||||
func (p *Container) Serve() (err error) {
|
func (p *Container) Serve() error {
|
||||||
if p.setup[0] == nil || p.setup[1] == nil {
|
if p.setup == nil {
|
||||||
panic("invalid serve")
|
panic("invalid serve")
|
||||||
}
|
}
|
||||||
|
|
||||||
done := make(chan struct{})
|
setup := p.setup
|
||||||
defer func() {
|
p.setup = nil
|
||||||
if closeErr := p.setup[1].Close(); err == nil {
|
|
||||||
err = closeErr
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
p.cancel()
|
|
||||||
}
|
|
||||||
close(done)
|
|
||||||
p.setup[0], p.setup[1] = nil, nil
|
|
||||||
}()
|
|
||||||
if err = p.setup[0].Close(); err != nil {
|
|
||||||
return &StartError{
|
|
||||||
Fatal: true,
|
|
||||||
Step: "close read end of init pipe",
|
|
||||||
Err: err,
|
|
||||||
Passthrough: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.Path == nil {
|
if p.Path == nil {
|
||||||
return &StartError{
|
p.cancel()
|
||||||
Step: "invalid executable pathname",
|
return &StartError{false, "invalid executable pathname", EINVAL, true, false}
|
||||||
Err: EINVAL,
|
|
||||||
Origin: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// do not transmit nil
|
// do not transmit nil
|
||||||
if p.Dir == nil {
|
if p.Dir == nil {
|
||||||
p.Dir = fhs.AbsRoot
|
p.Dir = AbsFHSRoot
|
||||||
}
|
}
|
||||||
if p.SeccompRules == nil {
|
if p.SeccompRules == nil {
|
||||||
p.SeccompRules = make([]std.NativeRule, 0)
|
p.SeccompRules = make([]seccomp.NativeRule, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
t := time.Now().UTC()
|
err := setup.Encode(
|
||||||
go func(f *os.File) {
|
&initParams{
|
||||||
select {
|
p.Params,
|
||||||
case <-p.ctx.Done():
|
Getuid(),
|
||||||
if cancelErr := f.SetWriteDeadline(t); cancelErr != nil {
|
Getgid(),
|
||||||
p.msg.Verbose(err)
|
len(p.ExtraFiles),
|
||||||
}
|
msg.IsVerbose(),
|
||||||
|
},
|
||||||
case <-done:
|
)
|
||||||
return
|
if err != nil {
|
||||||
}
|
p.cancel()
|
||||||
}(p.setup[1])
|
}
|
||||||
|
return err
|
||||||
return gob.NewEncoder(p.setup[1]).Encode(&initParams{
|
|
||||||
p.Params,
|
|
||||||
Getuid(),
|
|
||||||
Getgid(),
|
|
||||||
len(p.ExtraFiles),
|
|
||||||
p.msg.IsVerbose() && !p.Quiet,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait blocks until the container init process to exit and releases any
|
// Wait waits for the container init process to exit and releases any resources associated with the [Container].
|
||||||
// resources associated with the [Container].
|
|
||||||
func (p *Container) Wait() error {
|
func (p *Container) Wait() error {
|
||||||
if p.cmd == nil || p.cmd.Process == nil {
|
if p.cmd == nil {
|
||||||
return EINVAL
|
return EINVAL
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,44 +342,12 @@ func (p *Container) Wait() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// StdinPipe calls the [exec.Cmd] method with the same name.
|
|
||||||
func (p *Container) StdinPipe() (w io.WriteCloser, err error) {
|
|
||||||
if p.Stdin != nil {
|
|
||||||
return nil, errors.New("container: Stdin already set")
|
|
||||||
}
|
|
||||||
w, err = p.cmd.StdinPipe()
|
|
||||||
p.Stdin = p.cmd.Stdin
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// StdoutPipe calls the [exec.Cmd] method with the same name.
|
|
||||||
func (p *Container) StdoutPipe() (r io.ReadCloser, err error) {
|
|
||||||
if p.Stdout != nil {
|
|
||||||
return nil, errors.New("container: Stdout already set")
|
|
||||||
}
|
|
||||||
r, err = p.cmd.StdoutPipe()
|
|
||||||
p.Stdout = p.cmd.Stdout
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// StderrPipe calls the [exec.Cmd] method with the same name.
|
|
||||||
func (p *Container) StderrPipe() (r io.ReadCloser, err error) {
|
|
||||||
if p.Stderr != nil {
|
|
||||||
return nil, errors.New("container: Stderr already set")
|
|
||||||
}
|
|
||||||
r, err = p.cmd.StderrPipe()
|
|
||||||
p.Stderr = p.cmd.Stderr
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Container) String() string {
|
func (p *Container) String() string {
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x",
|
||||||
"argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x",
|
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets))
|
||||||
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProcessState returns the address of os.ProcessState held by the underlying [exec.Cmd].
|
// ProcessState returns the address to os.ProcessState held by the underlying [exec.Cmd].
|
||||||
func (p *Container) ProcessState() *os.ProcessState {
|
func (p *Container) ProcessState() *os.ProcessState {
|
||||||
if p.cmd == nil {
|
if p.cmd == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -576,29 +355,14 @@ func (p *Container) ProcessState() *os.ProcessState {
|
|||||||
return p.cmd.ProcessState
|
return p.cmd.ProcessState
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns the address to a new instance of [Container]. This value requires
|
// New returns the address to a new instance of [Container] that requires further initialisation before use.
|
||||||
// further initialisation before use.
|
func New(ctx context.Context) *Container {
|
||||||
func New(ctx context.Context, msg message.Msg) *Container {
|
return &Container{ctx: ctx, Params: Params{Ops: new(Ops)}}
|
||||||
if msg == nil {
|
|
||||||
msg = message.New(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
p := &Container{ctx: ctx, msg: msg, Params: Params{Ops: new(Ops)}}
|
|
||||||
c, cancel := context.WithCancel(ctx)
|
|
||||||
p.cancel = cancel
|
|
||||||
p.cmd = exec.CommandContext(c, fhs.ProcSelfExe)
|
|
||||||
return p
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields.
|
// NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields.
|
||||||
func NewCommand(
|
func NewCommand(ctx context.Context, pathname *Absolute, name string, args ...string) *Container {
|
||||||
ctx context.Context,
|
z := New(ctx)
|
||||||
msg message.Msg,
|
|
||||||
pathname *check.Absolute,
|
|
||||||
name string,
|
|
||||||
args ...string,
|
|
||||||
) *Container {
|
|
||||||
z := New(ctx, msg)
|
|
||||||
z.Path = pathname
|
z.Path = pathname
|
||||||
z.Args = append([]string{name}, args...)
|
z.Args = append([]string{name}, args...)
|
||||||
return z
|
return z
|
||||||
|
|||||||
+177
-367
@@ -17,66 +17,18 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
"hakurei.app/command"
|
"hakurei.app/command"
|
||||||
"hakurei.app/container"
|
"hakurei.app/container"
|
||||||
"hakurei.app/container/seccomp"
|
"hakurei.app/container/seccomp"
|
||||||
"hakurei.app/container/std"
|
"hakurei.app/container/vfs"
|
||||||
"hakurei.app/ext"
|
|
||||||
"hakurei.app/fhs"
|
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
"hakurei.app/internal/info"
|
"hakurei.app/internal"
|
||||||
"hakurei.app/internal/landlock"
|
"hakurei.app/internal/hlog"
|
||||||
"hakurei.app/internal/params"
|
|
||||||
"hakurei.app/ldd"
|
"hakurei.app/ldd"
|
||||||
"hakurei.app/message"
|
|
||||||
"hakurei.app/vfs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Note: this package requires cgo, which is unavailable in the Go playground.
|
|
||||||
func Example() {
|
|
||||||
// Must be called early if the current process starts containers.
|
|
||||||
container.TryArgv0(nil)
|
|
||||||
|
|
||||||
// Configure the container.
|
|
||||||
z := container.New(context.Background(), nil)
|
|
||||||
z.Hostname = "hakurei-example"
|
|
||||||
z.Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
|
|
||||||
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
|
|
||||||
|
|
||||||
// Bind / for demonstration.
|
|
||||||
z.Bind(fhs.AbsRoot, fhs.AbsRoot, 0)
|
|
||||||
if name, err := exec.LookPath("hostname"); err != nil {
|
|
||||||
panic(err)
|
|
||||||
} else {
|
|
||||||
z.Path = check.MustAbs(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This completes the first stage of container setup and starts the container init process.
|
|
||||||
// The new process blocks until the Serve method is called.
|
|
||||||
if err := z.Start(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This serves the setup payload to the container init process,
|
|
||||||
// starting the second stage of container setup.
|
|
||||||
if err := z.Serve(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Must be called if the Start method succeeds.
|
|
||||||
if err := z.Wait(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output: hakurei-example
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStartError(t *testing.T) {
|
func TestStartError(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
err error
|
err error
|
||||||
@@ -88,16 +40,18 @@ func TestStartError(t *testing.T) {
|
|||||||
{"params env", &container.StartError{
|
{"params env", &container.StartError{
|
||||||
Fatal: true,
|
Fatal: true,
|
||||||
Step: "set up params stream",
|
Step: "set up params stream",
|
||||||
Err: params.ErrReceiveEnv,
|
Err: container.ErrReceiveEnv,
|
||||||
}, "set up params stream: environment variable not set",
|
},
|
||||||
params.ErrReceiveEnv, syscall.EBADF,
|
"set up params stream: environment variable not set",
|
||||||
|
container.ErrReceiveEnv, syscall.EBADF,
|
||||||
"cannot set up params stream: environment variable not set"},
|
"cannot set up params stream: environment variable not set"},
|
||||||
|
|
||||||
{"params", &container.StartError{
|
{"params", &container.StartError{
|
||||||
Fatal: true,
|
Fatal: true,
|
||||||
Step: "set up params stream",
|
Step: "set up params stream",
|
||||||
Err: &os.SyscallError{Syscall: "pipe2", Err: syscall.EBADF},
|
Err: &os.SyscallError{Syscall: "pipe2", Err: syscall.EBADF},
|
||||||
}, "set up params stream pipe2: bad file descriptor",
|
},
|
||||||
|
"set up params stream pipe2: bad file descriptor",
|
||||||
syscall.EBADF, os.ErrInvalid,
|
syscall.EBADF, os.ErrInvalid,
|
||||||
"cannot set up params stream pipe2: bad file descriptor"},
|
"cannot set up params stream pipe2: bad file descriptor"},
|
||||||
|
|
||||||
@@ -105,14 +59,16 @@ func TestStartError(t *testing.T) {
|
|||||||
Fatal: true,
|
Fatal: true,
|
||||||
Step: "prctl(PR_SET_NO_NEW_PRIVS)",
|
Step: "prctl(PR_SET_NO_NEW_PRIVS)",
|
||||||
Err: syscall.EPERM,
|
Err: syscall.EPERM,
|
||||||
}, "prctl(PR_SET_NO_NEW_PRIVS): operation not permitted",
|
},
|
||||||
|
"prctl(PR_SET_NO_NEW_PRIVS): operation not permitted",
|
||||||
syscall.EPERM, syscall.EACCES,
|
syscall.EPERM, syscall.EACCES,
|
||||||
"cannot prctl(PR_SET_NO_NEW_PRIVS): operation not permitted"},
|
"cannot prctl(PR_SET_NO_NEW_PRIVS): operation not permitted"},
|
||||||
|
|
||||||
{"landlock abi", &container.StartError{
|
{"landlock abi", &container.StartError{
|
||||||
Step: "get landlock ABI",
|
Step: "get landlock ABI",
|
||||||
Err: syscall.ENOSYS,
|
Err: syscall.ENOSYS,
|
||||||
}, "get landlock ABI: function not implemented",
|
},
|
||||||
|
"get landlock ABI: function not implemented",
|
||||||
syscall.ENOSYS, syscall.ENOEXEC,
|
syscall.ENOSYS, syscall.ENOEXEC,
|
||||||
"cannot get landlock ABI: function not implemented"},
|
"cannot get landlock ABI: function not implemented"},
|
||||||
|
|
||||||
@@ -120,7 +76,8 @@ func TestStartError(t *testing.T) {
|
|||||||
Step: "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
|
Step: "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
|
||||||
Err: syscall.ENOSYS,
|
Err: syscall.ENOSYS,
|
||||||
Origin: true,
|
Origin: true,
|
||||||
}, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
|
},
|
||||||
|
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
|
||||||
syscall.ENOSYS, syscall.ENOSPC,
|
syscall.ENOSYS, syscall.ENOSPC,
|
||||||
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET"},
|
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET"},
|
||||||
|
|
||||||
@@ -128,7 +85,8 @@ func TestStartError(t *testing.T) {
|
|||||||
Fatal: true,
|
Fatal: true,
|
||||||
Step: "create landlock ruleset",
|
Step: "create landlock ruleset",
|
||||||
Err: syscall.EBADFD,
|
Err: syscall.EBADFD,
|
||||||
}, "create landlock ruleset: file descriptor in bad state",
|
},
|
||||||
|
"create landlock ruleset: file descriptor in bad state",
|
||||||
syscall.EBADFD, syscall.EBADF,
|
syscall.EBADFD, syscall.EBADF,
|
||||||
"cannot create landlock ruleset: file descriptor in bad state"},
|
"cannot create landlock ruleset: file descriptor in bad state"},
|
||||||
|
|
||||||
@@ -136,7 +94,8 @@ func TestStartError(t *testing.T) {
|
|||||||
Fatal: true,
|
Fatal: true,
|
||||||
Step: "enforce landlock ruleset",
|
Step: "enforce landlock ruleset",
|
||||||
Err: syscall.ENOTRECOVERABLE,
|
Err: syscall.ENOTRECOVERABLE,
|
||||||
}, "enforce landlock ruleset: state not recoverable",
|
},
|
||||||
|
"enforce landlock ruleset: state not recoverable",
|
||||||
syscall.ENOTRECOVERABLE, syscall.ETIMEDOUT,
|
syscall.ENOTRECOVERABLE, syscall.ETIMEDOUT,
|
||||||
"cannot enforce landlock ruleset: state not recoverable"},
|
"cannot enforce landlock ruleset: state not recoverable"},
|
||||||
|
|
||||||
@@ -147,7 +106,8 @@ func TestStartError(t *testing.T) {
|
|||||||
Path: "/proc/nonexistent",
|
Path: "/proc/nonexistent",
|
||||||
Err: syscall.ENOENT,
|
Err: syscall.ENOENT,
|
||||||
}, Passthrough: true,
|
}, Passthrough: true,
|
||||||
}, "fork/exec /proc/nonexistent: no such file or directory",
|
},
|
||||||
|
"fork/exec /proc/nonexistent: no such file or directory",
|
||||||
syscall.ENOENT, syscall.ENOSYS,
|
syscall.ENOENT, syscall.ENOSYS,
|
||||||
"cannot fork/exec /proc/nonexistent: no such file or directory"},
|
"cannot fork/exec /proc/nonexistent: no such file or directory"},
|
||||||
|
|
||||||
@@ -157,19 +117,11 @@ func TestStartError(t *testing.T) {
|
|||||||
Syscall: "open",
|
Syscall: "open",
|
||||||
Err: syscall.ENOSYS,
|
Err: syscall.ENOSYS,
|
||||||
}, Passthrough: true,
|
}, Passthrough: true,
|
||||||
}, "open: function not implemented",
|
},
|
||||||
|
"open: function not implemented",
|
||||||
syscall.ENOSYS, syscall.ENOENT,
|
syscall.ENOSYS, syscall.ENOENT,
|
||||||
"cannot open: function not implemented"},
|
"cannot open: function not implemented"},
|
||||||
|
|
||||||
{"start FD_CLOEXEC", &container.StartError{
|
|
||||||
Fatal: true,
|
|
||||||
Step: "set FD_CLOEXEC on all open files",
|
|
||||||
Err: func() error { _, err := strconv.Atoi("invalid"); return err }(),
|
|
||||||
Passthrough: true,
|
|
||||||
}, `strconv.Atoi: parsing "invalid": invalid syntax`,
|
|
||||||
strconv.ErrSyntax, os.ErrInvalid,
|
|
||||||
`cannot parse "invalid": invalid syntax`},
|
|
||||||
|
|
||||||
{"start other", &container.StartError{
|
{"start other", &container.StartError{
|
||||||
Step: "start container init",
|
Step: "start container init",
|
||||||
Err: &net.OpError{
|
Err: &net.OpError{
|
||||||
@@ -177,14 +129,13 @@ func TestStartError(t *testing.T) {
|
|||||||
Net: "unix",
|
Net: "unix",
|
||||||
Err: syscall.ECONNREFUSED,
|
Err: syscall.ECONNREFUSED,
|
||||||
}, Passthrough: true,
|
}, Passthrough: true,
|
||||||
}, "dial unix: connection refused",
|
},
|
||||||
|
"dial unix: connection refused",
|
||||||
syscall.ECONNREFUSED, syscall.ECONNABORTED,
|
syscall.ECONNREFUSED, syscall.ECONNABORTED,
|
||||||
"dial unix: connection refused"},
|
"dial unix: connection refused"},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("error", func(t *testing.T) {
|
t.Run("error", func(t *testing.T) {
|
||||||
if got := tc.err.Error(); got != tc.s {
|
if got := tc.err.Error(); got != tc.s {
|
||||||
t.Errorf("Error: %q, want %q", got, tc.s)
|
t.Errorf("Error: %q, want %q", got, tc.s)
|
||||||
@@ -201,13 +152,13 @@ func TestStartError(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("msg", func(t *testing.T) {
|
t.Run("msg", func(t *testing.T) {
|
||||||
if got, ok := message.GetMessage(tc.err); !ok {
|
if got, ok := container.GetErrorMessage(tc.err); !ok {
|
||||||
if tc.msg != "" {
|
if tc.msg != "" {
|
||||||
t.Errorf("GetMessage: err does not implement MessageError")
|
t.Errorf("GetErrorMessage: err does not implement MessageError")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
} else if got != tc.msg {
|
} else if got != tc.msg {
|
||||||
t.Errorf("GetMessage: %q, want %q", got, tc.msg)
|
t.Errorf("GetErrorMessage: %q, want %q", got, tc.msg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -235,9 +186,6 @@ func earlyMnt(mnt ...*vfs.MountInfoEntry) func(*testing.T, context.Context) []*v
|
|||||||
return func(*testing.T, context.Context) []*vfs.MountInfoEntry { return mnt }
|
return func(*testing.T, context.Context) []*vfs.MountInfoEntry { return mnt }
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:linkname toHost hakurei.app/container.toHost
|
|
||||||
func toHost(name string) string
|
|
||||||
|
|
||||||
var containerTestCases = []struct {
|
var containerTestCases = []struct {
|
||||||
name string
|
name string
|
||||||
filter bool
|
filter bool
|
||||||
@@ -251,83 +199,83 @@ var containerTestCases = []struct {
|
|||||||
uid int
|
uid int
|
||||||
gid int
|
gid int
|
||||||
|
|
||||||
rules []std.NativeRule
|
rules []seccomp.NativeRule
|
||||||
flags seccomp.ExportFlag
|
flags seccomp.ExportFlag
|
||||||
presets std.FilterPreset
|
presets seccomp.FilterPreset
|
||||||
}{
|
}{
|
||||||
{"minimal", true, false, false, true,
|
{"minimal", true, false, false, true,
|
||||||
emptyOps, emptyMnt,
|
emptyOps, emptyMnt,
|
||||||
1000, 100, nil, 0, std.PresetStrict},
|
1000, 100, nil, 0, seccomp.PresetStrict},
|
||||||
{"allow", true, true, true, false,
|
{"allow", true, true, true, false,
|
||||||
emptyOps, emptyMnt,
|
emptyOps, emptyMnt,
|
||||||
1000, 100, nil, 0, std.PresetExt | std.PresetDenyDevel},
|
1000, 100, nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel},
|
||||||
{"no filter", false, true, true, true,
|
{"no filter", false, true, true, true,
|
||||||
emptyOps, emptyMnt,
|
emptyOps, emptyMnt,
|
||||||
1000, 100, nil, 0, std.PresetExt},
|
1000, 100, nil, 0, seccomp.PresetExt},
|
||||||
{"custom rules", true, true, true, false,
|
{"custom rules", true, true, true, false,
|
||||||
emptyOps, emptyMnt,
|
emptyOps, emptyMnt,
|
||||||
1, 31, []std.NativeRule{{Syscall: ext.SyscallNum(syscall.SYS_SETUID), Errno: std.ScmpErrno(syscall.EPERM)}}, 0, std.PresetExt},
|
1, 31, []seccomp.NativeRule{{Syscall: seccomp.ScmpSyscall(syscall.SYS_SETUID), Errno: seccomp.ScmpErrno(syscall.EPERM)}}, 0, seccomp.PresetExt},
|
||||||
|
|
||||||
{"tmpfs", true, false, false, true,
|
{"tmpfs", true, false, false, true,
|
||||||
earlyOps(new(container.Ops).
|
earlyOps(new(container.Ops).
|
||||||
Tmpfs(hst.AbsPrivateTmp, 0, 0755),
|
Tmpfs(hst.AbsTmp, 0, 0755),
|
||||||
),
|
),
|
||||||
earlyMnt(
|
earlyMnt(
|
||||||
ent("/", hst.PrivateTmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
|
ent("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
|
||||||
),
|
),
|
||||||
9, 9, nil, 0, std.PresetStrict},
|
9, 9, nil, 0, seccomp.PresetStrict},
|
||||||
|
|
||||||
{"dev", true, true /* go test output is not a tty */, false, false,
|
{"dev", true, true /* go test output is not a tty */, false, false,
|
||||||
earlyOps(new(container.Ops).
|
earlyOps(new(container.Ops).
|
||||||
Dev(check.MustAbs("/dev"), true),
|
Dev(container.MustAbs("/dev"), true),
|
||||||
),
|
),
|
||||||
earlyMnt(
|
earlyMnt(
|
||||||
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", ignore, ignore),
|
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
|
||||||
ent("/null", "/dev/null", ignore, "devtmpfs", ignore, ignore),
|
ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||||
ent("/zero", "/dev/zero", ignore, "devtmpfs", ignore, ignore),
|
ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||||
ent("/full", "/dev/full", ignore, "devtmpfs", ignore, ignore),
|
ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||||
ent("/random", "/dev/random", ignore, "devtmpfs", ignore, ignore),
|
ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||||
ent("/urandom", "/dev/urandom", ignore, "devtmpfs", ignore, ignore),
|
ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||||
ent("/tty", "/dev/tty", ignore, "devtmpfs", ignore, ignore),
|
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||||
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
|
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
|
||||||
ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
|
ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
|
||||||
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
|
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
|
||||||
),
|
),
|
||||||
1971, 100, nil, 0, std.PresetStrict},
|
1971, 100, nil, 0, seccomp.PresetStrict},
|
||||||
|
|
||||||
{"dev no mqueue", true, true /* go test output is not a tty */, false, false,
|
{"dev no mqueue", true, true /* go test output is not a tty */, false, false,
|
||||||
earlyOps(new(container.Ops).
|
earlyOps(new(container.Ops).
|
||||||
Dev(check.MustAbs("/dev"), false),
|
Dev(container.MustAbs("/dev"), false),
|
||||||
),
|
),
|
||||||
earlyMnt(
|
earlyMnt(
|
||||||
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", ignore, ignore),
|
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
|
||||||
ent("/null", "/dev/null", ignore, "devtmpfs", ignore, ignore),
|
ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||||
ent("/zero", "/dev/zero", ignore, "devtmpfs", ignore, ignore),
|
ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||||
ent("/full", "/dev/full", ignore, "devtmpfs", ignore, ignore),
|
ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||||
ent("/random", "/dev/random", ignore, "devtmpfs", ignore, ignore),
|
ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||||
ent("/urandom", "/dev/urandom", ignore, "devtmpfs", ignore, ignore),
|
ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||||
ent("/tty", "/dev/tty", ignore, "devtmpfs", ignore, ignore),
|
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||||
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
|
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
|
||||||
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
|
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
|
||||||
),
|
),
|
||||||
1971, 100, nil, 0, std.PresetStrict},
|
1971, 100, nil, 0, seccomp.PresetStrict},
|
||||||
|
|
||||||
{"overlay", true, false, false, true,
|
{"overlay", true, false, false, true,
|
||||||
func(t *testing.T) (*container.Ops, context.Context) {
|
func(t *testing.T) (*container.Ops, context.Context) {
|
||||||
tempDir := check.MustAbs(t.TempDir())
|
tempDir := container.MustAbs(t.TempDir())
|
||||||
lower0, lower1, upper, work :=
|
lower0, lower1, upper, work :=
|
||||||
tempDir.Append("lower0"),
|
tempDir.Append("lower0"),
|
||||||
tempDir.Append("lower1"),
|
tempDir.Append("lower1"),
|
||||||
tempDir.Append("upper"),
|
tempDir.Append("upper"),
|
||||||
tempDir.Append("work")
|
tempDir.Append("work")
|
||||||
for _, a := range []*check.Absolute{lower0, lower1, upper, work} {
|
for _, a := range []*container.Absolute{lower0, lower1, upper, work} {
|
||||||
if err := os.Mkdir(a.String(), 0755); err != nil {
|
if err := os.Mkdir(a.String(), 0755); err != nil {
|
||||||
t.Fatalf("Mkdir: error = %v", err)
|
t.Fatalf("Mkdir: error = %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new(container.Ops).
|
return new(container.Ops).
|
||||||
Overlay(hst.AbsPrivateTmp, upper, work, lower0, lower1),
|
Overlay(hst.AbsTmp, upper, work, lower0, lower1),
|
||||||
context.WithValue(context.WithValue(context.WithValue(context.WithValue(t.Context(),
|
context.WithValue(context.WithValue(context.WithValue(context.WithValue(t.Context(),
|
||||||
testVal("lower1"), lower1),
|
testVal("lower1"), lower1),
|
||||||
testVal("lower0"), lower0),
|
testVal("lower0"), lower0),
|
||||||
@@ -336,91 +284,116 @@ var containerTestCases = []struct {
|
|||||||
},
|
},
|
||||||
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
|
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
|
||||||
return []*vfs.MountInfoEntry{
|
return []*vfs.MountInfoEntry{
|
||||||
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
|
ent("/", hst.Tmp, "rw", "overlay", "overlay",
|
||||||
"rw"+
|
"rw,lowerdir="+
|
||||||
",lowerdir+="+
|
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
|
||||||
toHost(ctx.Value(testVal("lower0")).(*check.Absolute).String())+
|
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
|
||||||
",lowerdir+="+
|
|
||||||
toHost(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
|
|
||||||
",upperdir="+
|
",upperdir="+
|
||||||
toHost(ctx.Value(testVal("upper")).(*check.Absolute).String())+
|
container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*container.Absolute).String())+
|
||||||
",workdir="+
|
",workdir="+
|
||||||
toHost(ctx.Value(testVal("work")).(*check.Absolute).String())+
|
container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*container.Absolute).String())+
|
||||||
",redirect_dir=nofollow,uuid=on,userxattr"),
|
",redirect_dir=nofollow,uuid=on,userxattr"),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
1 << 3, 1 << 14, nil, 0, std.PresetStrict},
|
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
|
||||||
|
|
||||||
{"overlay ephemeral", true, false, false, true,
|
{"overlay ephemeral", true, false, false, true,
|
||||||
func(t *testing.T) (*container.Ops, context.Context) {
|
func(t *testing.T) (*container.Ops, context.Context) {
|
||||||
tempDir := check.MustAbs(t.TempDir())
|
tempDir := container.MustAbs(t.TempDir())
|
||||||
lower0, lower1 :=
|
lower0, lower1 :=
|
||||||
tempDir.Append("lower0"),
|
tempDir.Append("lower0"),
|
||||||
tempDir.Append("lower1")
|
tempDir.Append("lower1")
|
||||||
for _, a := range []*check.Absolute{lower0, lower1} {
|
for _, a := range []*container.Absolute{lower0, lower1} {
|
||||||
if err := os.Mkdir(a.String(), 0755); err != nil {
|
if err := os.Mkdir(a.String(), 0755); err != nil {
|
||||||
t.Fatalf("Mkdir: error = %v", err)
|
t.Fatalf("Mkdir: error = %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new(container.Ops).
|
return new(container.Ops).
|
||||||
OverlayEphemeral(hst.AbsPrivateTmp, lower0, lower1),
|
OverlayEphemeral(hst.AbsTmp, lower0, lower1),
|
||||||
t.Context()
|
t.Context()
|
||||||
},
|
},
|
||||||
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
|
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
|
||||||
return []*vfs.MountInfoEntry{
|
return []*vfs.MountInfoEntry{
|
||||||
// contains random suffix
|
// contains random suffix
|
||||||
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay", ignore),
|
ent("/", hst.Tmp, "rw", "overlay", "overlay", ignore),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
1 << 3, 1 << 14, nil, 0, std.PresetStrict},
|
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
|
||||||
|
|
||||||
{"overlay readonly", true, false, false, true,
|
{"overlay readonly", true, false, false, true,
|
||||||
func(t *testing.T) (*container.Ops, context.Context) {
|
func(t *testing.T) (*container.Ops, context.Context) {
|
||||||
tempDir := check.MustAbs(t.TempDir())
|
tempDir := container.MustAbs(t.TempDir())
|
||||||
lower0, lower1 :=
|
lower0, lower1 :=
|
||||||
tempDir.Append("lower0"),
|
tempDir.Append("lower0"),
|
||||||
tempDir.Append("lower1")
|
tempDir.Append("lower1")
|
||||||
for _, a := range []*check.Absolute{lower0, lower1} {
|
for _, a := range []*container.Absolute{lower0, lower1} {
|
||||||
if err := os.Mkdir(a.String(), 0755); err != nil {
|
if err := os.Mkdir(a.String(), 0755); err != nil {
|
||||||
t.Fatalf("Mkdir: error = %v", err)
|
t.Fatalf("Mkdir: error = %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new(container.Ops).
|
return new(container.Ops).
|
||||||
OverlayReadonly(hst.AbsPrivateTmp, lower0, lower1),
|
OverlayReadonly(hst.AbsTmp, lower0, lower1),
|
||||||
context.WithValue(context.WithValue(t.Context(),
|
context.WithValue(context.WithValue(t.Context(),
|
||||||
testVal("lower1"), lower1),
|
testVal("lower1"), lower1),
|
||||||
testVal("lower0"), lower0)
|
testVal("lower0"), lower0)
|
||||||
},
|
},
|
||||||
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
|
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
|
||||||
return []*vfs.MountInfoEntry{
|
return []*vfs.MountInfoEntry{
|
||||||
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
|
ent("/", hst.Tmp, "rw", "overlay", "overlay",
|
||||||
"ro"+
|
"ro,lowerdir="+
|
||||||
",lowerdir+="+
|
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
|
||||||
toHost(ctx.Value(testVal("lower0")).(*check.Absolute).String())+
|
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
|
||||||
",lowerdir+="+
|
|
||||||
toHost(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
|
|
||||||
",redirect_dir=nofollow,userxattr"),
|
",redirect_dir=nofollow,userxattr"),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
1 << 3, 1 << 14, nil, 0, std.PresetStrict},
|
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContainer(t *testing.T) {
|
func TestContainer(t *testing.T) {
|
||||||
t.Parallel()
|
replaceOutput(t)
|
||||||
|
|
||||||
|
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
|
||||||
|
wantErr := context.Canceled
|
||||||
|
wantExitCode := 0
|
||||||
|
if err := c.Wait(); !reflect.DeepEqual(err, wantErr) {
|
||||||
|
if m, ok := container.InternalMessageFromError(err); ok {
|
||||||
|
t.Error(m)
|
||||||
|
}
|
||||||
|
t.Errorf("Wait: error = %#v, want %#v", err, wantErr)
|
||||||
|
}
|
||||||
|
if ps := c.ProcessState(); ps == nil {
|
||||||
|
t.Errorf("ProcessState unexpectedly returned nil")
|
||||||
|
} else if code := ps.ExitCode(); code != wantExitCode {
|
||||||
|
t.Errorf("ExitCode: %d, want %d", code, wantExitCode)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
t.Run("forward", testContainerCancel(func(c *container.Container) {
|
||||||
|
c.ForwardCancel = true
|
||||||
|
}, func(t *testing.T, c *container.Container) {
|
||||||
|
var exitError *exec.ExitError
|
||||||
|
if err := c.Wait(); !errors.As(err, &exitError) {
|
||||||
|
if m, ok := container.InternalMessageFromError(err); ok {
|
||||||
|
t.Error(m)
|
||||||
|
}
|
||||||
|
t.Errorf("Wait: error = %v", err)
|
||||||
|
}
|
||||||
|
if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
|
||||||
|
t.Errorf("ExitCode: %d, want %d", code, blockExitCodeInterrupt)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
var suffix string
|
|
||||||
runTests:
|
|
||||||
for i, tc := range containerTestCases {
|
for i, tc := range containerTestCases {
|
||||||
_suffix := suffix
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Run(tc.name+_suffix, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
wantOps, wantOpsCtx := tc.ops(t)
|
wantOps, wantOpsCtx := tc.ops(t)
|
||||||
wantMnt := tc.mnt(t, wantOpsCtx)
|
wantMnt := tc.mnt(t, wantOpsCtx)
|
||||||
|
|
||||||
var libPaths []*check.Absolute
|
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
|
||||||
c := helperNewContainerLibPaths(t.Context(), &libPaths, "container", strconv.Itoa(i))
|
defer cancel()
|
||||||
|
|
||||||
|
var libPaths []*container.Absolute
|
||||||
|
c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i))
|
||||||
c.Uid = tc.uid
|
c.Uid = tc.uid
|
||||||
c.Gid = tc.gid
|
c.Gid = tc.gid
|
||||||
c.Hostname = hostnameFromTestCase(tc.name)
|
c.Hostname = hostnameFromTestCase(tc.name)
|
||||||
@@ -430,6 +403,7 @@ runTests:
|
|||||||
} else {
|
} else {
|
||||||
c.Stdout, c.Stderr = os.Stdout, os.Stderr
|
c.Stdout, c.Stderr = os.Stdout, os.Stderr
|
||||||
}
|
}
|
||||||
|
c.WaitDelay = helperDefaultTimeout
|
||||||
*c.Ops = append(*c.Ops, *wantOps...)
|
*c.Ops = append(*c.Ops, *wantOps...)
|
||||||
c.SeccompRules = tc.rules
|
c.SeccompRules = tc.rules
|
||||||
c.SeccompFlags = tc.flags | seccomp.AllowMultiarch
|
c.SeccompFlags = tc.flags | seccomp.AllowMultiarch
|
||||||
@@ -437,27 +411,13 @@ runTests:
|
|||||||
c.SeccompDisable = !tc.filter
|
c.SeccompDisable = !tc.filter
|
||||||
c.RetainSession = tc.session
|
c.RetainSession = tc.session
|
||||||
c.HostNet = tc.net
|
c.HostNet = tc.net
|
||||||
c.InitAsRoot = _suffix != ""
|
|
||||||
c.Env = append(c.Env, "HAKUREI_TEST_SUFFIX="+_suffix)
|
|
||||||
if info.CanDegrade {
|
|
||||||
if _, err := landlock.GetABI(); err != nil {
|
|
||||||
if !errors.Is(err, syscall.ENOSYS) {
|
|
||||||
t.Fatalf("LandlockGetABI: error = %v", err)
|
|
||||||
}
|
|
||||||
c.HostAbstract = true
|
|
||||||
t.Log("Landlock LSM is unavailable, enabling HostAbstract")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if c.InitAsRoot {
|
|
||||||
c.SeccompPresets &= ^std.PresetDenyNS
|
|
||||||
}
|
|
||||||
|
|
||||||
c.
|
c.
|
||||||
Readonly(check.MustAbs(pathReadonly), 0755).
|
Readonly(container.MustAbs(pathReadonly), 0755).
|
||||||
Tmpfs(check.MustAbs("/tmp"), 0, 0755).
|
Tmpfs(container.MustAbs("/tmp"), 0, 0755).
|
||||||
Place(check.MustAbs("/etc/hostname"), []byte(c.Hostname))
|
Place(container.MustAbs("/etc/hostname"), []byte(c.Hostname))
|
||||||
// needs /proc to check mountinfo
|
// needs /proc to check mountinfo
|
||||||
c.Proc(check.MustAbs("/proc"))
|
c.Proc(container.MustAbs("/proc"))
|
||||||
|
|
||||||
// mountinfo cannot be resolved directly by helper due to libPaths nondeterminism
|
// mountinfo cannot be resolved directly by helper due to libPaths nondeterminism
|
||||||
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
|
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
|
||||||
@@ -488,10 +448,10 @@ runTests:
|
|||||||
_, _ = output.WriteTo(os.Stdout)
|
_, _ = output.WriteTo(os.Stdout)
|
||||||
t.Fatalf("cannot serialise expected mount points: %v", err)
|
t.Fatalf("cannot serialise expected mount points: %v", err)
|
||||||
}
|
}
|
||||||
c.Place(check.MustAbs(pathWantMnt), want.Bytes())
|
c.Place(container.MustAbs(pathWantMnt), want.Bytes())
|
||||||
|
|
||||||
if tc.ro {
|
if tc.ro {
|
||||||
c.Remount(check.MustAbs("/"), syscall.MS_RDONLY)
|
c.Remount(container.MustAbs("/"), syscall.MS_RDONLY)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.Start(); err != nil {
|
if err := c.Start(); err != nil {
|
||||||
@@ -519,11 +479,6 @@ runTests:
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if suffix == "" {
|
|
||||||
suffix = " as root"
|
|
||||||
goto runTests
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ent(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
|
func ent(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
|
||||||
@@ -546,129 +501,58 @@ func hostnameFromTestCase(name string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testContainerCancel(
|
func testContainerCancel(
|
||||||
t *testing.T,
|
|
||||||
containerExtra func(c *container.Container),
|
containerExtra func(c *container.Container),
|
||||||
waitCheck func(ps *os.ProcessState, waitErr error),
|
waitCheck func(t *testing.T, c *container.Container),
|
||||||
) {
|
) func(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(t.Context())
|
return func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
|
||||||
|
|
||||||
c := helperNewContainer(ctx, "block")
|
c := helperNewContainer(ctx, "block")
|
||||||
c.Stdout, c.Stderr = os.Stdout, os.Stderr
|
c.Stdout, c.Stderr = os.Stdout, os.Stderr
|
||||||
if containerExtra != nil {
|
c.WaitDelay = helperDefaultTimeout
|
||||||
containerExtra(c)
|
if containerExtra != nil {
|
||||||
}
|
containerExtra(c)
|
||||||
|
|
||||||
ready := make(chan struct{})
|
|
||||||
var waitErr error
|
|
||||||
r, w, err := os.Pipe()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("cannot pipe: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.ExtraFiles = append(c.ExtraFiles, w)
|
|
||||||
go func() {
|
|
||||||
defer close(ready)
|
|
||||||
if _, _err := r.Read(make([]byte, 1)); _err != nil {
|
|
||||||
panic(_err)
|
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
if err = c.Start(); err != nil {
|
ready := make(chan struct{})
|
||||||
if m, ok := container.InternalMessageFromError(err); ok {
|
if r, w, err := os.Pipe(); err != nil {
|
||||||
t.Fatal(m)
|
t.Fatalf("cannot pipe: %v", err)
|
||||||
} else {
|
} else {
|
||||||
t.Fatalf("cannot start container: %v", err)
|
c.ExtraFiles = append(c.ExtraFiles, w)
|
||||||
|
go func() {
|
||||||
|
defer close(ready)
|
||||||
|
if _, err = r.Read(make([]byte, 1)); err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
if err := c.Start(); err != nil {
|
||||||
go func() {
|
if m, ok := container.InternalMessageFromError(err); ok {
|
||||||
defer close(done)
|
t.Fatal(m)
|
||||||
waitErr = c.Wait()
|
} else {
|
||||||
_ = r.SetReadDeadline(time.Now())
|
t.Fatalf("cannot start container: %v", err)
|
||||||
}()
|
|
||||||
|
|
||||||
if err = c.Serve(); err != nil {
|
|
||||||
if m, ok := container.InternalMessageFromError(err); ok {
|
|
||||||
t.Error(m)
|
|
||||||
} else {
|
|
||||||
t.Errorf("cannot serve setup params: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
<-ready
|
|
||||||
cancel()
|
|
||||||
<-done
|
|
||||||
waitCheck(c.ProcessState(), waitErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestForward(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
f := func(ps *os.ProcessState, waitErr error) {
|
|
||||||
var exitError *exec.ExitError
|
|
||||||
if !errors.As(waitErr, &exitError) {
|
|
||||||
if m, ok := container.InternalMessageFromError(waitErr); ok {
|
|
||||||
t.Error(m)
|
|
||||||
}
|
}
|
||||||
t.Errorf("Wait: error = %v", waitErr)
|
} else if err = c.Serve(); err != nil {
|
||||||
}
|
if m, ok := container.InternalMessageFromError(err); ok {
|
||||||
if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
|
|
||||||
t.Errorf("ExitCode: %d, want %d", code, blockExitCodeInterrupt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t.Run("direct", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
testContainerCancel(t, func(c *container.Container) {
|
|
||||||
c.ForwardCancel = true
|
|
||||||
}, f)
|
|
||||||
})
|
|
||||||
t.Run("as root", func(t *testing.T) {
|
|
||||||
testContainerCancel(t, func(c *container.Container) {
|
|
||||||
c.ForwardCancel = true
|
|
||||||
c.InitAsRoot = true
|
|
||||||
c.Proc(fhs.AbsProc)
|
|
||||||
}, f)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCancel(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
f := func(ps *os.ProcessState, waitErr error) {
|
|
||||||
wantErr := context.Canceled
|
|
||||||
if !reflect.DeepEqual(waitErr, wantErr) {
|
|
||||||
if m, ok := container.InternalMessageFromError(waitErr); ok {
|
|
||||||
t.Error(m)
|
t.Error(m)
|
||||||
|
} else {
|
||||||
|
t.Errorf("cannot serve setup params: %v", err)
|
||||||
}
|
}
|
||||||
t.Errorf("Wait: error = %#v, want %#v", waitErr, wantErr)
|
|
||||||
}
|
|
||||||
if ps == nil {
|
|
||||||
t.Errorf("ProcessState unexpectedly returned nil")
|
|
||||||
} else if code := ps.ExitCode(); code != 0 {
|
|
||||||
t.Errorf("ExitCode: %d, want %d", code, 0)
|
|
||||||
}
|
}
|
||||||
|
<-ready
|
||||||
|
cancel()
|
||||||
|
waitCheck(t, c)
|
||||||
}
|
}
|
||||||
t.Run("direct", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
testContainerCancel(t, nil, f)
|
|
||||||
})
|
|
||||||
t.Run("as root", func(t *testing.T) {
|
|
||||||
testContainerCancel(t, func(c *container.Container) {
|
|
||||||
c.InitAsRoot = true
|
|
||||||
c.Proc(fhs.AbsProc)
|
|
||||||
}, f)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContainerString(t *testing.T) {
|
func TestContainerString(t *testing.T) {
|
||||||
t.Parallel()
|
c := container.NewCommand(t.Context(), container.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env")
|
||||||
msg := message.New(nil)
|
|
||||||
c := container.NewCommand(t.Context(), msg, check.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env")
|
|
||||||
c.SeccompFlags |= seccomp.AllowMultiarch
|
c.SeccompFlags |= seccomp.AllowMultiarch
|
||||||
c.SeccompRules = seccomp.Preset(
|
c.SeccompRules = seccomp.Preset(
|
||||||
std.PresetExt|std.PresetDenyNS|std.PresetDenyTTY,
|
seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY,
|
||||||
c.SeccompFlags)
|
c.SeccompFlags)
|
||||||
c.SeccompPresets = std.PresetStrict
|
c.SeccompPresets = seccomp.PresetStrict
|
||||||
want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf`
|
want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf`
|
||||||
if got := c.String(); got != want {
|
if got := c.String(); got != want {
|
||||||
t.Errorf("String: %s, want %s", got, want)
|
t.Errorf("String: %s, want %s", got, want)
|
||||||
@@ -682,19 +566,18 @@ const (
|
|||||||
func init() {
|
func init() {
|
||||||
helperCommands = append(helperCommands, func(c command.Command) {
|
helperCommands = append(helperCommands, func(c command.Command) {
|
||||||
c.Command("block", command.UsageInternal, func(args []string) error {
|
c.Command("block", command.UsageInternal, func(args []string) error {
|
||||||
sig := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sig, os.Interrupt)
|
|
||||||
go func() { <-sig; os.Exit(blockExitCodeInterrupt) }()
|
|
||||||
|
|
||||||
if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil {
|
if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil {
|
||||||
return fmt.Errorf("write to sync pipe: %v", err)
|
return fmt.Errorf("write to sync pipe: %v", err)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
sig := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sig, os.Interrupt)
|
||||||
|
go func() { <-sig; os.Exit(blockExitCodeInterrupt) }()
|
||||||
|
}
|
||||||
select {}
|
select {}
|
||||||
})
|
})
|
||||||
|
|
||||||
c.Command("container", command.UsageInternal, func(args []string) error {
|
c.Command("container", command.UsageInternal, func(args []string) error {
|
||||||
asRoot := os.Getenv("HAKUREI_TEST_SUFFIX") == " as root"
|
|
||||||
|
|
||||||
if len(args) != 1 {
|
if len(args) != 1 {
|
||||||
return syscall.EINVAL
|
return syscall.EINVAL
|
||||||
}
|
}
|
||||||
@@ -712,66 +595,6 @@ func init() {
|
|||||||
return fmt.Errorf("gid: %d, want %d", gid, tc.gid)
|
return fmt.Errorf("gid: %d, want %d", gid, tc.gid)
|
||||||
}
|
}
|
||||||
|
|
||||||
// no attack surface increase during as root due to no_new_privs
|
|
||||||
var wantBounding uintptr = 1
|
|
||||||
asRootNot := " not"
|
|
||||||
if !asRoot {
|
|
||||||
wantBounding = 0
|
|
||||||
asRootNot = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
PR_CAP_AMBIENT = 0x2f
|
|
||||||
PR_CAP_AMBIENT_IS_SET = 0x1
|
|
||||||
)
|
|
||||||
for i := range container.LastCap(nil) + 1 {
|
|
||||||
r, _, errno := syscall.Syscall(
|
|
||||||
syscall.SYS_PRCTL,
|
|
||||||
PR_CAP_AMBIENT,
|
|
||||||
PR_CAP_AMBIENT_IS_SET,
|
|
||||||
i,
|
|
||||||
)
|
|
||||||
if errno != 0 {
|
|
||||||
return os.NewSyscallError("prctl", errno)
|
|
||||||
}
|
|
||||||
if r != 0 {
|
|
||||||
return fmt.Errorf("capability %d in ambient set", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
r, _, errno = syscall.Syscall(
|
|
||||||
syscall.SYS_PRCTL,
|
|
||||||
syscall.PR_CAPBSET_READ,
|
|
||||||
i,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
if errno != 0 {
|
|
||||||
return os.NewSyscallError("prctl", errno)
|
|
||||||
}
|
|
||||||
if r != wantBounding {
|
|
||||||
return fmt.Errorf("capability %d%s in bounding set", i, asRootNot)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const _LINUX_CAPABILITY_VERSION_3 = 0x20080522
|
|
||||||
var capData struct {
|
|
||||||
effective uint32
|
|
||||||
permitted uint32
|
|
||||||
inheritable uint32
|
|
||||||
}
|
|
||||||
if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&struct {
|
|
||||||
version uint32
|
|
||||||
pid int32
|
|
||||||
}{_LINUX_CAPABILITY_VERSION_3, 0})), uintptr(unsafe.Pointer(&capData)), 0); errno != 0 {
|
|
||||||
return os.NewSyscallError("capget", errno)
|
|
||||||
}
|
|
||||||
|
|
||||||
if max(capData.effective, capData.permitted, capData.inheritable) != 0 {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"effective = %d, permitted = %d, inheritable = %d",
|
|
||||||
capData.effective, capData.permitted, capData.inheritable,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
wantHost := hostnameFromTestCase(tc.name)
|
wantHost := hostnameFromTestCase(tc.name)
|
||||||
if host, err := os.Hostname(); err != nil {
|
if host, err := os.Hostname(); err != nil {
|
||||||
return fmt.Errorf("cannot get hostname: %v", err)
|
return fmt.Errorf("cannot get hostname: %v", err)
|
||||||
@@ -819,22 +642,11 @@ func init() {
|
|||||||
return fmt.Errorf("got more than %d entries", len(mnt))
|
return fmt.Errorf("got more than %d entries", len(mnt))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ugly hack but should be reliable and is less likely to
|
// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
|
||||||
//false negative than comparing by parsed flags
|
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime")
|
||||||
for _, s := range []string{
|
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime")
|
||||||
"relatime",
|
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime")
|
||||||
"noatime",
|
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
|
||||||
} {
|
|
||||||
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ","+s)
|
|
||||||
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ","+s)
|
|
||||||
}
|
|
||||||
for _, s := range []string{
|
|
||||||
"seclabel",
|
|
||||||
"inode64",
|
|
||||||
} {
|
|
||||||
cur.FsOptstr = strings.Replace(cur.FsOptstr, ","+s, "", 1)
|
|
||||||
mnt[i].FsOptstr = strings.Replace(mnt[i].FsOptstr, ","+s, "", 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cur.EqualWithIgnore(mnt[i], "\x00") {
|
if !cur.EqualWithIgnore(mnt[i], "\x00") {
|
||||||
fail = true
|
fail = true
|
||||||
@@ -866,17 +678,18 @@ func init() {
|
|||||||
const (
|
const (
|
||||||
envDoCheck = "HAKUREI_TEST_DO_CHECK"
|
envDoCheck = "HAKUREI_TEST_DO_CHECK"
|
||||||
|
|
||||||
helperInnerPath = "/usr/bin/helper"
|
helperDefaultTimeout = 5 * time.Second
|
||||||
|
helperInnerPath = "/usr/bin/helper"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
absHelperInnerPath = check.MustAbs(helperInnerPath)
|
absHelperInnerPath = container.MustAbs(helperInnerPath)
|
||||||
)
|
)
|
||||||
|
|
||||||
var helperCommands []func(c command.Command)
|
var helperCommands []func(c command.Command)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
container.TryArgv0(nil)
|
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
|
||||||
|
|
||||||
if os.Getenv(envDoCheck) == "1" {
|
if os.Getenv(envDoCheck) == "1" {
|
||||||
c := command.New(os.Stderr, log.Printf, "helper", func(args []string) error {
|
c := command.New(os.Stderr, log.Printf, "helper", func(args []string) error {
|
||||||
@@ -889,7 +702,7 @@ func TestMain(m *testing.M) {
|
|||||||
}
|
}
|
||||||
c.MustParse(os.Args[1:], func(err error) {
|
c.MustParse(os.Args[1:], func(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err.Error())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -898,16 +711,13 @@ func TestMain(m *testing.M) {
|
|||||||
os.Exit(m.Run())
|
os.Exit(m.Run())
|
||||||
}
|
}
|
||||||
|
|
||||||
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute, args ...string) (c *container.Container) {
|
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*container.Absolute, args ...string) (c *container.Container) {
|
||||||
msg := message.New(nil)
|
c = container.NewCommand(ctx, absHelperInnerPath, "helper", args...)
|
||||||
msg.SwapVerbose(testing.Verbose())
|
|
||||||
|
|
||||||
c = container.NewCommand(ctx, msg, absHelperInnerPath, "helper", args...)
|
|
||||||
c.Env = append(c.Env, envDoCheck+"=1")
|
c.Env = append(c.Env, envDoCheck+"=1")
|
||||||
c.Bind(fhs.AbsProcSelfExe, absHelperInnerPath, 0)
|
c.Bind(container.MustAbs(os.Args[0]), absHelperInnerPath, 0)
|
||||||
|
|
||||||
// in case test has cgo enabled
|
// in case test has cgo enabled
|
||||||
if entries, err := ldd.Resolve(ctx, msg, nil); err != nil {
|
if entries, err := ldd.Exec(ctx, os.Args[0]); err != nil {
|
||||||
log.Fatalf("ldd: %v", err)
|
log.Fatalf("ldd: %v", err)
|
||||||
} else {
|
} else {
|
||||||
*libPaths = ldd.Path(entries)
|
*libPaths = ldd.Path(entries)
|
||||||
@@ -920,5 +730,5 @@ func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute
|
|||||||
}
|
}
|
||||||
|
|
||||||
func helperNewContainer(ctx context.Context, args ...string) (c *container.Container) {
|
func helperNewContainer(ctx context.Context, args ...string) (c *container.Container) {
|
||||||
return helperNewContainerLibPaths(ctx, new([]*check.Absolute), args...)
|
return helperNewContainerLibPaths(ctx, new([]*container.Absolute), args...)
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-86
@@ -1,10 +1,9 @@
|
|||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -13,11 +12,6 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"hakurei.app/container/seccomp"
|
"hakurei.app/container/seccomp"
|
||||||
"hakurei.app/container/std"
|
|
||||||
"hakurei.app/ext"
|
|
||||||
"hakurei.app/internal/netlink"
|
|
||||||
"hakurei.app/internal/params"
|
|
||||||
"hakurei.app/message"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type osFile interface {
|
type osFile interface {
|
||||||
@@ -26,8 +20,7 @@ type osFile interface {
|
|||||||
fs.File
|
fs.File
|
||||||
}
|
}
|
||||||
|
|
||||||
// syscallDispatcher provides methods that make state-dependent system calls as
|
// syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour.
|
||||||
// part of their behaviour.
|
|
||||||
type syscallDispatcher interface {
|
type syscallDispatcher interface {
|
||||||
// new starts a goroutine with a new instance of syscallDispatcher.
|
// new starts a goroutine with a new instance of syscallDispatcher.
|
||||||
// A syscallDispatcher must never be used in any goroutine other than the one owning it,
|
// A syscallDispatcher must never be used in any goroutine other than the one owning it,
|
||||||
@@ -45,7 +38,7 @@ type syscallDispatcher interface {
|
|||||||
setNoNewPrivs() error
|
setNoNewPrivs() error
|
||||||
|
|
||||||
// lastcap provides [LastCap].
|
// lastcap provides [LastCap].
|
||||||
lastcap(msg message.Msg) uintptr
|
lastcap() uintptr
|
||||||
// capset provides capset.
|
// capset provides capset.
|
||||||
capset(hdrp *capHeader, datap *[2]capData) error
|
capset(hdrp *capHeader, datap *[2]capData) error
|
||||||
// capBoundingSetDrop provides capBoundingSetDrop.
|
// capBoundingSetDrop provides capBoundingSetDrop.
|
||||||
@@ -57,23 +50,19 @@ type syscallDispatcher interface {
|
|||||||
// isatty provides [Isatty].
|
// isatty provides [Isatty].
|
||||||
isatty(fd int) bool
|
isatty(fd int) bool
|
||||||
// receive provides [Receive].
|
// receive provides [Receive].
|
||||||
receive(key string, e any, fdp *int) (closeFunc func() error, err error)
|
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
|
||||||
|
|
||||||
// bindMount provides procPaths.bindMount.
|
// bindMount provides procPaths.bindMount.
|
||||||
bindMount(msg message.Msg, source, target string, flags uintptr) error
|
bindMount(source, target string, flags uintptr, eq bool) error
|
||||||
// remount provides procPaths.remount.
|
// remount provides procPaths.remount.
|
||||||
remount(msg message.Msg, target string, flags uintptr) error
|
remount(target string, flags uintptr) error
|
||||||
// mountTmpfs provides mountTmpfs.
|
// mountTmpfs provides mountTmpfs.
|
||||||
mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error
|
mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error
|
||||||
// mountOverlay provides mountOverlay.
|
|
||||||
mountOverlay(target string, options [][2]string) error
|
|
||||||
// ensureFile provides ensureFile.
|
// ensureFile provides ensureFile.
|
||||||
ensureFile(name string, perm, pperm os.FileMode) error
|
ensureFile(name string, perm, pperm os.FileMode) error
|
||||||
// mustLoopback provides mustLoopback.
|
|
||||||
mustLoopback(ctx context.Context, msg message.Msg)
|
|
||||||
|
|
||||||
// seccompLoad provides [seccomp.Load].
|
// seccompLoad provides [seccomp.Load].
|
||||||
seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error
|
seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error
|
||||||
// notify provides [signal.Notify].
|
// notify provides [signal.Notify].
|
||||||
notify(c chan<- os.Signal, sig ...os.Signal)
|
notify(c chan<- os.Signal, sig ...os.Signal)
|
||||||
// start starts [os/exec.Cmd].
|
// start starts [os/exec.Cmd].
|
||||||
@@ -133,12 +122,22 @@ type syscallDispatcher interface {
|
|||||||
// wait4 provides syscall.Wait4
|
// wait4 provides syscall.Wait4
|
||||||
wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error)
|
wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error)
|
||||||
|
|
||||||
// printf provides the Printf method of [log.Logger].
|
// printf provides [log.Printf].
|
||||||
printf(msg message.Msg, format string, v ...any)
|
printf(format string, v ...any)
|
||||||
// fatal provides the Fatal method of [log.Logger]
|
// fatal provides [log.Fatal]
|
||||||
fatal(msg message.Msg, v ...any)
|
fatal(v ...any)
|
||||||
// fatalf provides the Fatalf method of [log.Logger]
|
// fatalf provides [log.Fatalf]
|
||||||
fatalf(msg message.Msg, format string, v ...any)
|
fatalf(format string, v ...any)
|
||||||
|
// verbose provides [Msg.Verbose].
|
||||||
|
verbose(v ...any)
|
||||||
|
// verbosef provides [Msg.Verbosef].
|
||||||
|
verbosef(format string, v ...any)
|
||||||
|
// suspend provides [Msg.Suspend].
|
||||||
|
suspend()
|
||||||
|
// resume provides [Msg.Resume].
|
||||||
|
resume() bool
|
||||||
|
// beforeExit provides [Msg.BeforeExit].
|
||||||
|
beforeExit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// direct implements syscallDispatcher on the current kernel.
|
// direct implements syscallDispatcher on the current kernel.
|
||||||
@@ -148,81 +147,34 @@ func (k direct) new(f func(k syscallDispatcher)) { go f(k) }
|
|||||||
|
|
||||||
func (direct) lockOSThread() { runtime.LockOSThread() }
|
func (direct) lockOSThread() { runtime.LockOSThread() }
|
||||||
|
|
||||||
func (direct) setPtracer(pid uintptr) error { return ext.SetPtracer(pid) }
|
func (direct) setPtracer(pid uintptr) error { return SetPtracer(pid) }
|
||||||
func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) }
|
func (direct) setDumpable(dumpable uintptr) error { return SetDumpable(dumpable) }
|
||||||
func (direct) setNoNewPrivs() error { return setNoNewPrivs() }
|
func (direct) setNoNewPrivs() error { return SetNoNewPrivs() }
|
||||||
|
|
||||||
func (direct) lastcap(msg message.Msg) uintptr { return LastCap(msg) }
|
func (direct) lastcap() uintptr { return LastCap() }
|
||||||
func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }
|
func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }
|
||||||
func (direct) capBoundingSetDrop(cap uintptr) error { return capBoundingSetDrop(cap) }
|
func (direct) capBoundingSetDrop(cap uintptr) error { return capBoundingSetDrop(cap) }
|
||||||
func (direct) capAmbientClearAll() error { return capAmbientClearAll() }
|
func (direct) capAmbientClearAll() error { return capAmbientClearAll() }
|
||||||
func (direct) capAmbientRaise(cap uintptr) error { return capAmbientRaise(cap) }
|
func (direct) capAmbientRaise(cap uintptr) error { return capAmbientRaise(cap) }
|
||||||
func (direct) isatty(fd int) bool { return ext.Isatty(fd) }
|
func (direct) isatty(fd int) bool { return Isatty(fd) }
|
||||||
func (direct) receive(key string, e any, fdp *int) (func() error, error) {
|
func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
|
||||||
return params.Receive(key, e, fdp)
|
return Receive(key, e, fdp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error {
|
func (direct) bindMount(source, target string, flags uintptr, eq bool) error {
|
||||||
return hostProc.bindMount(msg, source, target, flags)
|
return hostProc.bindMount(source, target, flags, eq)
|
||||||
}
|
}
|
||||||
func (direct) remount(msg message.Msg, target string, flags uintptr) error {
|
func (direct) remount(target string, flags uintptr) error {
|
||||||
return hostProc.remount(msg, target, flags)
|
return hostProc.remount(target, flags)
|
||||||
}
|
}
|
||||||
func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
|
func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
|
||||||
return mountTmpfs(k, fsname, target, flags, size, perm)
|
return mountTmpfs(k, fsname, target, flags, size, perm)
|
||||||
}
|
}
|
||||||
func (k direct) mountOverlay(target string, options [][2]string) error {
|
|
||||||
return mountOverlay(target, options)
|
|
||||||
}
|
|
||||||
func (direct) ensureFile(name string, perm, pperm os.FileMode) error {
|
func (direct) ensureFile(name string, perm, pperm os.FileMode) error {
|
||||||
return ensureFile(name, perm, pperm)
|
return ensureFile(name, perm, pperm)
|
||||||
}
|
}
|
||||||
func (direct) mustLoopback(ctx context.Context, msg message.Msg) {
|
|
||||||
var lo int
|
|
||||||
if ifi, err := net.InterfaceByName("lo"); err != nil {
|
|
||||||
msg.GetLogger().Fatalln(err)
|
|
||||||
} else {
|
|
||||||
lo = ifi.Index
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := netlink.DialRoute(0)
|
func (direct) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
|
||||||
if err != nil {
|
|
||||||
msg.GetLogger().Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
must := func(err error) {
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if closeErr := c.Close(); closeErr != nil {
|
|
||||||
msg.Verbosef("cannot close RTNETLINK: %v", closeErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch err.(type) {
|
|
||||||
case *os.SyscallError:
|
|
||||||
msg.GetLogger().Fatalf("cannot %v", err)
|
|
||||||
|
|
||||||
case syscall.Errno:
|
|
||||||
msg.GetLogger().Fatalf("RTNETLINK answers: %v", err)
|
|
||||||
|
|
||||||
default:
|
|
||||||
if err == context.DeadlineExceeded || err == context.Canceled {
|
|
||||||
msg.GetLogger().Fatalf("interrupted RTNETLINK operation")
|
|
||||||
}
|
|
||||||
msg.GetLogger().Fatal("RTNETLINK answers with malformed message")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
must(c.SendNewaddrLo(ctx, uint32(lo)))
|
|
||||||
must(c.SendIfInfomsg(ctx, syscall.RTM_NEWLINK, 0, &syscall.IfInfomsg{
|
|
||||||
Family: syscall.AF_UNSPEC,
|
|
||||||
Index: int32(lo),
|
|
||||||
Flags: syscall.IFF_UP,
|
|
||||||
Change: syscall.IFF_UP,
|
|
||||||
}))
|
|
||||||
must(c.Close())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (direct) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error {
|
|
||||||
return seccomp.Load(rules, flags)
|
return seccomp.Load(rules, flags)
|
||||||
}
|
}
|
||||||
func (direct) notify(c chan<- os.Signal, sig ...os.Signal) { signal.Notify(c, sig...) }
|
func (direct) notify(c chan<- os.Signal, sig ...os.Signal) { signal.Notify(c, sig...) }
|
||||||
@@ -280,6 +232,11 @@ func (direct) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *s
|
|||||||
return syscall.Wait4(pid, wstatus, options, rusage)
|
return syscall.Wait4(pid, wstatus, options, rusage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (direct) printf(msg message.Msg, format string, v ...any) { msg.GetLogger().Printf(format, v...) }
|
func (direct) printf(format string, v ...any) { log.Printf(format, v...) }
|
||||||
func (direct) fatal(msg message.Msg, v ...any) { msg.GetLogger().Fatal(v...) }
|
func (direct) fatal(v ...any) { log.Fatal(v...) }
|
||||||
func (direct) fatalf(msg message.Msg, format string, v ...any) { msg.GetLogger().Fatalf(format, v...) }
|
func (direct) fatalf(format string, v ...any) { log.Fatalf(format, v...) }
|
||||||
|
func (direct) verbose(v ...any) { msg.Verbose(v...) }
|
||||||
|
func (direct) verbosef(format string, v ...any) { msg.Verbosef(format, v...) }
|
||||||
|
func (direct) suspend() { msg.Suspend() }
|
||||||
|
func (direct) resume() bool { return msg.Resume() }
|
||||||
|
func (direct) beforeExit() { msg.BeforeExit() }
|
||||||
|
|||||||
+27
-109
@@ -2,11 +2,8 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -18,9 +15,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hakurei.app/container/seccomp"
|
"hakurei.app/container/seccomp"
|
||||||
"hakurei.app/container/std"
|
"hakurei.app/container/stub"
|
||||||
"hakurei.app/internal/stub"
|
|
||||||
"hakurei.app/message"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type opValidTestCase struct {
|
type opValidTestCase struct {
|
||||||
@@ -34,12 +29,10 @@ func checkOpsValid(t *testing.T, testCases []opValidTestCase) {
|
|||||||
|
|
||||||
t.Run("valid", func(t *testing.T) {
|
t.Run("valid", func(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
if got := tc.op.Valid(); got != tc.want {
|
if got := tc.op.Valid(); got != tc.want {
|
||||||
t.Errorf("Valid: %v, want %v", got, tc.want)
|
t.Errorf("Valid: %v, want %v", got, tc.want)
|
||||||
@@ -60,12 +53,10 @@ func checkOpsBuilder(t *testing.T, testCases []opsBuilderTestCase) {
|
|||||||
|
|
||||||
t.Run("build", func(t *testing.T) {
|
t.Run("build", func(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
if !slices.EqualFunc(*tc.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) {
|
if !slices.EqualFunc(*tc.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) {
|
||||||
t.Errorf("Ops: %#v, want %#v", tc.ops, tc.want)
|
t.Errorf("Ops: %#v, want %#v", tc.ops, tc.want)
|
||||||
@@ -86,12 +77,10 @@ func checkOpIs(t *testing.T, testCases []opIsTestCase) {
|
|||||||
|
|
||||||
t.Run("is", func(t *testing.T) {
|
t.Run("is", func(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
if got := tc.op.Is(tc.v); got != tc.want {
|
if got := tc.op.Is(tc.v); got != tc.want {
|
||||||
t.Errorf("Is: %v, want %v", got, tc.want)
|
t.Errorf("Is: %v, want %v", got, tc.want)
|
||||||
@@ -114,17 +103,15 @@ func checkOpMeta(t *testing.T, testCases []opMetaTestCase) {
|
|||||||
|
|
||||||
t.Run("meta", func(t *testing.T) {
|
t.Run("meta", func(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("prefix", func(t *testing.T) {
|
t.Run("prefix", func(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
if got, _ := tc.op.prefix(); got != tc.wantPrefix {
|
if got := tc.op.prefix(); got != tc.wantPrefix {
|
||||||
t.Errorf("prefix: %q, want %q", got, tc.wantPrefix)
|
t.Errorf("prefix: %q, want %q", got, tc.wantPrefix)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -149,7 +136,7 @@ func call(name string, args stub.ExpectArgs, ret any, err error) stub.Call {
|
|||||||
|
|
||||||
type simpleTestCase struct {
|
type simpleTestCase struct {
|
||||||
name string
|
name string
|
||||||
f func(k *kstub) error
|
f func(k syscallDispatcher) error
|
||||||
want stub.Expect
|
want stub.Expect
|
||||||
wantErr error
|
wantErr error
|
||||||
}
|
}
|
||||||
@@ -160,11 +147,9 @@ func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
|
|||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
wait4signal := make(chan struct{})
|
wait4signal := make(chan struct{})
|
||||||
lockNotify := make(chan struct{})
|
k := &kstub{wait4signal, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, s} }, tc.want)}
|
||||||
k := &kstub{wait4signal, lockNotify, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{wait4signal, lockNotify, s} }, tc.want)}
|
|
||||||
defer stub.HandleExit(t)
|
defer stub.HandleExit(t)
|
||||||
if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) {
|
if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) {
|
||||||
t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr)
|
t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr)
|
||||||
@@ -195,18 +180,16 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
|
|||||||
|
|
||||||
t.Run("behaviour", func(t *testing.T) {
|
t.Run("behaviour", func(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
k := &kstub{nil, nil, stub.New(t,
|
state := &setupState{Params: tc.params}
|
||||||
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, nil, s} },
|
k := &kstub{nil, stub.New(t,
|
||||||
|
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{nil, s} },
|
||||||
stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)},
|
stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)},
|
||||||
)}
|
)}
|
||||||
state := &setupState{Params: tc.params, Msg: k}
|
|
||||||
defer stub.HandleExit(t)
|
defer stub.HandleExit(t)
|
||||||
errEarly := tc.op.early(state, k)
|
errEarly := tc.op.early(state, k)
|
||||||
k.Expects(stub.CallSeparator)
|
k.Expects(stub.CallSeparator)
|
||||||
@@ -239,11 +222,8 @@ func sliceAddr[S any](s []S) *[]S { return &s }
|
|||||||
|
|
||||||
func newCheckedFile(t *testing.T, name, wantData string, closeErr error) osFile {
|
func newCheckedFile(t *testing.T, name, wantData string, closeErr error) osFile {
|
||||||
f := &checkedOsFile{t: t, name: name, want: wantData, closeErr: closeErr}
|
f := &checkedOsFile{t: t, name: name, want: wantData, closeErr: closeErr}
|
||||||
// check happens in Close, and cleanup is not guaranteed to run, so relying
|
// check happens in Close, and cleanup is not guaranteed to run, so relying on it for sloppy implementations will cause sporadic test results
|
||||||
// on it for sloppy implementations will cause sporadic test results
|
f.cleanup = runtime.AddCleanup(f, func(name string) { f.t.Fatalf("checkedOsFile %s became unreachable without a call to Close", name) }, f.name)
|
||||||
f.cleanup = runtime.AddCleanup(f, func(name string) {
|
|
||||||
panic("checkedOsFile " + name + " became unreachable without a call to Close")
|
|
||||||
}, name)
|
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,19 +307,12 @@ const (
|
|||||||
|
|
||||||
type kstub struct {
|
type kstub struct {
|
||||||
wait4signal chan struct{}
|
wait4signal chan struct{}
|
||||||
lockNotify chan struct{}
|
|
||||||
*stub.Stub[syscallDispatcher]
|
*stub.Stub[syscallDispatcher]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) }
|
func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) }
|
||||||
|
|
||||||
func (k *kstub) lockOSThread() {
|
func (k *kstub) lockOSThread() { k.Helper(); k.Expects("lockOSThread") }
|
||||||
k.Helper()
|
|
||||||
expect := k.Expects("lockOSThread")
|
|
||||||
if k.lockNotify != nil && expect.Ret == magicWait4Signal {
|
|
||||||
<-k.lockNotify
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *kstub) setPtracer(pid uintptr) error {
|
func (k *kstub) setPtracer(pid uintptr) error {
|
||||||
k.Helper()
|
k.Helper()
|
||||||
@@ -354,11 +327,7 @@ func (k *kstub) setDumpable(dumpable uintptr) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) setNoNewPrivs() error { k.Helper(); return k.Expects("setNoNewPrivs").Err }
|
func (k *kstub) setNoNewPrivs() error { k.Helper(); return k.Expects("setNoNewPrivs").Err }
|
||||||
func (k *kstub) lastcap(msg message.Msg) uintptr {
|
func (k *kstub) lastcap() uintptr { k.Helper(); return k.Expects("lastcap").Ret.(uintptr) }
|
||||||
k.Helper()
|
|
||||||
k.checkMsg(msg)
|
|
||||||
return k.Expects("lastcap").Ret.(uintptr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *kstub) capset(hdrp *capHeader, datap *[2]capData) error {
|
func (k *kstub) capset(hdrp *capHeader, datap *[2]capData) error {
|
||||||
k.Helper()
|
k.Helper()
|
||||||
@@ -390,7 +359,7 @@ func (k *kstub) isatty(fd int) bool {
|
|||||||
return expect.Ret.(bool)
|
return expect.Ret.(bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) receive(key string, e any, fdp *int) (closeFunc func() error, err error) {
|
func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
|
||||||
k.Helper()
|
k.Helper()
|
||||||
expect := k.Expects("receive")
|
expect := k.Expects("receive")
|
||||||
|
|
||||||
@@ -408,17 +377,10 @@ func (k *kstub) receive(key string, e any, fdp *int) (closeFunc func() error, er
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// avoid changing test cases
|
|
||||||
var fdpComp *uintptr
|
|
||||||
if fdp != nil {
|
|
||||||
fdpComp = new(uintptr(*fdp))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = expect.Error(
|
err = expect.Error(
|
||||||
stub.CheckArg(k.Stub, "key", key, 0),
|
stub.CheckArg(k.Stub, "key", key, 0),
|
||||||
stub.CheckArgReflect(k.Stub, "e", e, 1),
|
stub.CheckArgReflect(k.Stub, "e", e, 1),
|
||||||
stub.CheckArgReflect(k.Stub, "fdp", fdpComp, 2))
|
stub.CheckArgReflect(k.Stub, "fdp", fdp, 2))
|
||||||
|
|
||||||
// 3 is unused so stores params
|
// 3 is unused so stores params
|
||||||
if expect.Args[3] != nil {
|
if expect.Args[3] != nil {
|
||||||
@@ -433,7 +395,7 @@ func (k *kstub) receive(key string, e any, fdp *int) (closeFunc func() error, er
|
|||||||
if expect.Args[4] != nil {
|
if expect.Args[4] != nil {
|
||||||
if v, ok := expect.Args[4].(uintptr); ok && v >= 3 {
|
if v, ok := expect.Args[4].(uintptr); ok && v >= 3 {
|
||||||
if fdp != nil {
|
if fdp != nil {
|
||||||
*fdp = int(v)
|
*fdp = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -441,18 +403,17 @@ func (k *kstub) receive(key string, e any, fdp *int) (closeFunc func() error, er
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) bindMount(msg message.Msg, source, target string, flags uintptr) error {
|
func (k *kstub) bindMount(source, target string, flags uintptr, eq bool) error {
|
||||||
k.Helper()
|
k.Helper()
|
||||||
k.checkMsg(msg)
|
|
||||||
return k.Expects("bindMount").Error(
|
return k.Expects("bindMount").Error(
|
||||||
stub.CheckArg(k.Stub, "source", source, 0),
|
stub.CheckArg(k.Stub, "source", source, 0),
|
||||||
stub.CheckArg(k.Stub, "target", target, 1),
|
stub.CheckArg(k.Stub, "target", target, 1),
|
||||||
stub.CheckArg(k.Stub, "flags", flags, 2))
|
stub.CheckArg(k.Stub, "flags", flags, 2),
|
||||||
|
stub.CheckArg(k.Stub, "eq", eq, 3))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) remount(msg message.Msg, target string, flags uintptr) error {
|
func (k *kstub) remount(target string, flags uintptr) error {
|
||||||
k.Helper()
|
k.Helper()
|
||||||
k.checkMsg(msg)
|
|
||||||
return k.Expects("remount").Error(
|
return k.Expects("remount").Error(
|
||||||
stub.CheckArg(k.Stub, "target", target, 0),
|
stub.CheckArg(k.Stub, "target", target, 0),
|
||||||
stub.CheckArg(k.Stub, "flags", flags, 1))
|
stub.CheckArg(k.Stub, "flags", flags, 1))
|
||||||
@@ -468,14 +429,6 @@ func (k *kstub) mountTmpfs(fsname, target string, flags uintptr, size int, perm
|
|||||||
stub.CheckArg(k.Stub, "perm", perm, 4))
|
stub.CheckArg(k.Stub, "perm", perm, 4))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) mountOverlay(target string, options [][2]string) error {
|
|
||||||
k.Helper()
|
|
||||||
return k.Expects("mountOverlay").Error(
|
|
||||||
stub.CheckArg(k.Stub, "target", target, 0),
|
|
||||||
stub.CheckArgReflect(k.Stub, "options", options, 1),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
|
func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
|
||||||
k.Helper()
|
k.Helper()
|
||||||
return k.Expects("ensureFile").Error(
|
return k.Expects("ensureFile").Error(
|
||||||
@@ -484,9 +437,7 @@ func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
|
|||||||
stub.CheckArg(k.Stub, "pperm", pperm, 2))
|
stub.CheckArg(k.Stub, "pperm", pperm, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*kstub) mustLoopback(context.Context, message.Msg) { /* noop */ }
|
func (k *kstub) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
|
||||||
|
|
||||||
func (k *kstub) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error {
|
|
||||||
k.Helper()
|
k.Helper()
|
||||||
return k.Expects("seccompLoad").Error(
|
return k.Expects("seccompLoad").Error(
|
||||||
stub.CheckArgReflect(k.Stub, "rules", rules, 0),
|
stub.CheckArgReflect(k.Stub, "rules", rules, 0),
|
||||||
@@ -501,10 +452,6 @@ func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) {
|
|||||||
k.FailNow()
|
k.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
if k.lockNotify != nil && expect.Ret == magicWait4Signal {
|
|
||||||
defer close(k.lockNotify)
|
|
||||||
}
|
|
||||||
|
|
||||||
// export channel for external instrumentation
|
// export channel for external instrumentation
|
||||||
if chanf, ok := expect.Args[0].(func(c chan<- os.Signal)); ok && chanf != nil {
|
if chanf, ok := expect.Args[0].(func(c chan<- os.Signal)); ok && chanf != nil {
|
||||||
chanf(c)
|
chanf(c)
|
||||||
@@ -748,7 +695,7 @@ func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) printf(_ message.Msg, format string, v ...any) {
|
func (k *kstub) printf(format string, v ...any) {
|
||||||
k.Helper()
|
k.Helper()
|
||||||
if k.Expects("printf").Error(
|
if k.Expects("printf").Error(
|
||||||
stub.CheckArg(k.Stub, "format", format, 0),
|
stub.CheckArg(k.Stub, "format", format, 0),
|
||||||
@@ -757,7 +704,7 @@ func (k *kstub) printf(_ message.Msg, format string, v ...any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) fatal(_ message.Msg, v ...any) {
|
func (k *kstub) fatal(v ...any) {
|
||||||
k.Helper()
|
k.Helper()
|
||||||
if k.Expects("fatal").Error(
|
if k.Expects("fatal").Error(
|
||||||
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
|
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
|
||||||
@@ -766,7 +713,7 @@ func (k *kstub) fatal(_ message.Msg, v ...any) {
|
|||||||
panic(stub.PanicExit)
|
panic(stub.PanicExit)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) fatalf(_ message.Msg, format string, v ...any) {
|
func (k *kstub) fatalf(format string, v ...any) {
|
||||||
k.Helper()
|
k.Helper()
|
||||||
if k.Expects("fatalf").Error(
|
if k.Expects("fatalf").Error(
|
||||||
stub.CheckArg(k.Stub, "format", format, 0),
|
stub.CheckArg(k.Stub, "format", format, 0),
|
||||||
@@ -776,36 +723,7 @@ func (k *kstub) fatalf(_ message.Msg, format string, v ...any) {
|
|||||||
panic(stub.PanicExit)
|
panic(stub.PanicExit)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) checkMsg(msg message.Msg) {
|
func (k *kstub) verbose(v ...any) {
|
||||||
k.Helper()
|
|
||||||
var target *kstub
|
|
||||||
|
|
||||||
if state, ok := msg.(*setupState); ok {
|
|
||||||
target = state.Msg.(*kstub)
|
|
||||||
} else {
|
|
||||||
target = msg.(*kstub)
|
|
||||||
}
|
|
||||||
|
|
||||||
if k != target {
|
|
||||||
panic(fmt.Sprintf("unexpected Msg: %#v", msg))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *kstub) GetLogger() *log.Logger { panic("unreachable") }
|
|
||||||
|
|
||||||
func (k *kstub) IsVerbose() bool { k.Helper(); return k.Expects("isVerbose").Ret.(bool) }
|
|
||||||
|
|
||||||
func (k *kstub) SwapVerbose(verbose bool) bool {
|
|
||||||
k.Helper()
|
|
||||||
expect := k.Expects("swapVerbose")
|
|
||||||
if expect.Error(
|
|
||||||
stub.CheckArg(k.Stub, "verbose", verbose, 0)) != nil {
|
|
||||||
k.FailNow()
|
|
||||||
}
|
|
||||||
return expect.Ret.(bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *kstub) Verbose(v ...any) {
|
|
||||||
k.Helper()
|
k.Helper()
|
||||||
if k.Expects("verbose").Error(
|
if k.Expects("verbose").Error(
|
||||||
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
|
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
|
||||||
@@ -813,7 +731,7 @@ func (k *kstub) Verbose(v ...any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) Verbosef(format string, v ...any) {
|
func (k *kstub) verbosef(format string, v ...any) {
|
||||||
k.Helper()
|
k.Helper()
|
||||||
if k.Expects("verbosef").Error(
|
if k.Expects("verbosef").Error(
|
||||||
stub.CheckArg(k.Stub, "format", format, 0),
|
stub.CheckArg(k.Stub, "format", format, 0),
|
||||||
@@ -822,6 +740,6 @@ func (k *kstub) Verbosef(format string, v ...any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kstub) Suspend() bool { k.Helper(); return k.Expects("suspend").Ret.(bool) }
|
func (k *kstub) suspend() { k.Helper(); k.Expects("suspend") }
|
||||||
func (k *kstub) Resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) }
|
func (k *kstub) resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) }
|
||||||
func (k *kstub) BeforeExit() { k.Helper(); k.Expects("beforeExit") }
|
func (k *kstub) beforeExit() { k.Helper(); k.Expects("beforeExit") }
|
||||||
|
|||||||
+17
-39
@@ -5,46 +5,39 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"hakurei.app/check"
|
"hakurei.app/container/vfs"
|
||||||
"hakurei.app/message"
|
|
||||||
"hakurei.app/vfs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// messageFromError returns a printable error message for a supported concrete type.
|
// messageFromError returns a printable error message for a supported concrete type.
|
||||||
func messageFromError(err error) (m string, ok bool) {
|
func messageFromError(err error) (string, bool) {
|
||||||
if m, ok = messagePrefixP[MountError]("cannot ", err); ok {
|
if m, ok := messagePrefixP[MountError]("cannot ", err); ok {
|
||||||
return
|
return m, ok
|
||||||
}
|
}
|
||||||
if m, ok = messagePrefixP[os.PathError]("cannot ", err); ok {
|
if m, ok := messagePrefixP[os.PathError]("cannot ", err); ok {
|
||||||
return
|
return m, ok
|
||||||
}
|
}
|
||||||
if m, ok = messagePrefix[check.AbsoluteError](zeroString, err); ok {
|
if m, ok := messagePrefixP[AbsoluteError]("", err); ok {
|
||||||
return
|
return m, ok
|
||||||
}
|
}
|
||||||
if m, ok = messagePrefix[OpRepeatError](zeroString, err); ok {
|
if m, ok := messagePrefix[OpRepeatError]("", err); ok {
|
||||||
return
|
return m, ok
|
||||||
}
|
}
|
||||||
if m, ok = messagePrefix[OpStateError](zeroString, err); ok {
|
if m, ok := messagePrefix[OpStateError]("", err); ok {
|
||||||
return
|
return m, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
if m, ok = messagePrefixP[vfs.DecoderError]("cannot ", err); ok {
|
if m, ok := messagePrefixP[vfs.DecoderError]("cannot ", err); ok {
|
||||||
return
|
return m, ok
|
||||||
}
|
}
|
||||||
if m, ok = messagePrefix[TmpfsSizeError](zeroString, err); ok {
|
if m, ok := messagePrefix[TmpfsSizeError]("", err); ok {
|
||||||
return
|
return m, ok
|
||||||
}
|
|
||||||
|
|
||||||
if m, ok = message.GetMessage(err); ok {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return zeroString, false
|
return zeroString, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// messagePrefix checks and prefixes the error message of a non-pointer error.
|
// messagePrefix checks and prefixes the error message of a non-pointer error.
|
||||||
// While this is usable for pointer errors, such use should be avoided as nil
|
// While this is usable for pointer errors, such use should be avoided as nil check is omitted.
|
||||||
// check is omitted.
|
|
||||||
func messagePrefix[T error](prefix string, err error) (string, bool) {
|
func messagePrefix[T error](prefix string, err error) (string, bool) {
|
||||||
var targetError T
|
var targetError T
|
||||||
if errors.As(err, &targetError) {
|
if errors.As(err, &targetError) {
|
||||||
@@ -65,7 +58,6 @@ func messagePrefixP[V any, T interface {
|
|||||||
return zeroString, false
|
return zeroString, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// MountError wraps errors returned by syscall.Mount.
|
|
||||||
type MountError struct {
|
type MountError struct {
|
||||||
Source, Target, Fstype string
|
Source, Target, Fstype string
|
||||||
|
|
||||||
@@ -81,7 +73,6 @@ func (e *MountError) Unwrap() error {
|
|||||||
return e.Errno
|
return e.Errno
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *MountError) Message() string { return "cannot " + e.Error() }
|
|
||||||
func (e *MountError) Error() string {
|
func (e *MountError) Error() string {
|
||||||
if e.Flags&syscall.MS_BIND != 0 {
|
if e.Flags&syscall.MS_BIND != 0 {
|
||||||
if e.Flags&syscall.MS_REMOUNT != 0 {
|
if e.Flags&syscall.MS_REMOUNT != 0 {
|
||||||
@@ -98,15 +89,6 @@ func (e *MountError) Error() string {
|
|||||||
return "mount " + e.Target + ": " + e.Errno.Error()
|
return "mount " + e.Target + ": " + e.Errno.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
// optionalErrorUnwrap calls [errors.Unwrap] and returns the resulting value
|
|
||||||
// if it is not nil, or the original value if it is.
|
|
||||||
func optionalErrorUnwrap(err error) error {
|
|
||||||
if underlyingErr := errors.Unwrap(err); underlyingErr != nil {
|
|
||||||
return underlyingErr
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// errnoFallback returns the concrete errno from an error, or a [os.PathError] fallback.
|
// errnoFallback returns the concrete errno from an error, or a [os.PathError] fallback.
|
||||||
func errnoFallback(op, path string, err error) (syscall.Errno, *os.PathError) {
|
func errnoFallback(op, path string, err error) (syscall.Errno, *os.PathError) {
|
||||||
var errno syscall.Errno
|
var errno syscall.Errno
|
||||||
@@ -118,10 +100,6 @@ func errnoFallback(op, path string, err error) (syscall.Errno, *os.PathError) {
|
|||||||
|
|
||||||
// mount wraps syscall.Mount for error handling.
|
// mount wraps syscall.Mount for error handling.
|
||||||
func mount(source, target, fstype string, flags uintptr, data string) error {
|
func mount(source, target, fstype string, flags uintptr, data string) error {
|
||||||
if max(len(source), len(target), len(data))+1 > os.Getpagesize() {
|
|
||||||
return &MountError{source, target, fstype, flags, data, syscall.ENOMEM}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := syscall.Mount(source, target, fstype, flags, data)
|
err := syscall.Mount(source, target, fstype, flags, data)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -8,14 +8,11 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"hakurei.app/check"
|
"hakurei.app/container/stub"
|
||||||
"hakurei.app/internal/stub"
|
"hakurei.app/container/vfs"
|
||||||
"hakurei.app/vfs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMessageFromError(t *testing.T) {
|
func TestMessageFromError(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
err error
|
err error
|
||||||
@@ -37,7 +34,7 @@ func TestMessageFromError(t *testing.T) {
|
|||||||
Err: stub.UniqueError(0xdeadbeef),
|
Err: stub.UniqueError(0xdeadbeef),
|
||||||
}, "cannot mount /sysroot: unique error 3735928559 injected by the test suite", true},
|
}, "cannot mount /sysroot: unique error 3735928559 injected by the test suite", true},
|
||||||
|
|
||||||
{"absolute", check.AbsoluteError("etc/mtab"),
|
{"absolute", &AbsoluteError{"etc/mtab"},
|
||||||
`path "etc/mtab" is not absolute`, true},
|
`path "etc/mtab" is not absolute`, true},
|
||||||
|
|
||||||
{"repeat", OpRepeatError("autoetc"),
|
{"repeat", OpRepeatError("autoetc"),
|
||||||
@@ -46,8 +43,8 @@ func TestMessageFromError(t *testing.T) {
|
|||||||
{"state", OpStateError("overlay"),
|
{"state", OpStateError("overlay"),
|
||||||
"impossible overlay state reached", true},
|
"impossible overlay state reached", true},
|
||||||
|
|
||||||
{"vfs parse", &vfs.DecoderError{Op: "parse", Line: 0xdead, Err: &strconv.NumError{Func: "Atoi", Num: "meow", Err: strconv.ErrSyntax}},
|
{"vfs parse", &vfs.DecoderError{Op: "parse", Line: 0xdeadbeef, Err: &strconv.NumError{Func: "Atoi", Num: "meow", Err: strconv.ErrSyntax}},
|
||||||
`cannot parse mountinfo at line 57005: numeric field "meow" invalid syntax`, true},
|
`cannot parse mountinfo at line 3735928559: numeric field "meow" invalid syntax`, true},
|
||||||
|
|
||||||
{"tmpfs", TmpfsSizeError(-1),
|
{"tmpfs", TmpfsSizeError(-1),
|
||||||
"tmpfs size -1 out of bounds", true},
|
"tmpfs size -1 out of bounds", true},
|
||||||
@@ -56,7 +53,6 @@ func TestMessageFromError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
got, ok := messageFromError(tc.err)
|
got, ok := messageFromError(tc.err)
|
||||||
if got != tc.want {
|
if got != tc.want {
|
||||||
t.Errorf("messageFromError: %q, want %q", got, tc.want)
|
t.Errorf("messageFromError: %q, want %q", got, tc.want)
|
||||||
@@ -69,8 +65,6 @@ func TestMessageFromError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMountError(t *testing.T) {
|
func TestMountError(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
err error
|
err error
|
||||||
@@ -116,7 +110,6 @@ func TestMountError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
t.Run("is", func(t *testing.T) {
|
t.Run("is", func(t *testing.T) {
|
||||||
if !errors.Is(tc.err, tc.errno) {
|
if !errors.Is(tc.err, tc.errno) {
|
||||||
t.Errorf("Is: %#v is not %v", tc.err, tc.errno)
|
t.Errorf("Is: %#v is not %v", tc.err, tc.errno)
|
||||||
@@ -131,7 +124,6 @@ func TestMountError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Run("zero", func(t *testing.T) {
|
t.Run("zero", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
if errors.Is(new(MountError), syscall.Errno(0)) {
|
if errors.Is(new(MountError), syscall.Errno(0)) {
|
||||||
t.Errorf("Is: zero MountError unexpected true")
|
t.Errorf("Is: zero MountError unexpected true")
|
||||||
}
|
}
|
||||||
@@ -139,8 +131,6 @@ func TestMountError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestErrnoFallback(t *testing.T) {
|
func TestErrnoFallback(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
err error
|
err error
|
||||||
@@ -163,7 +153,6 @@ func TestErrnoFallback(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
errno, err := errnoFallback(tc.name, Nonexistent, tc.err)
|
errno, err := errnoFallback(tc.name, Nonexistent, tc.err)
|
||||||
if errno != tc.wantErrno {
|
if errno != tc.wantErrno {
|
||||||
t.Errorf("errnoFallback: errno = %v, want %v", errno, tc.wantErrno)
|
t.Errorf("errnoFallback: errno = %v, want %v", errno, tc.wantErrno)
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
executable string
|
||||||
|
executableOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func copyExecutable() {
|
||||||
|
if name, err := os.Executable(); err != nil {
|
||||||
|
msg.BeforeExit()
|
||||||
|
log.Fatalf("cannot read executable path: %v", err)
|
||||||
|
} else {
|
||||||
|
executable = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustExecutable() string {
|
||||||
|
executableOnce.Do(copyExecutable)
|
||||||
|
return executable
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package container_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExecutable(t *testing.T) {
|
||||||
|
for i := 0; i < 16; i++ {
|
||||||
|
if got := container.MustExecutable(); got != os.Args[0] {
|
||||||
|
t.Errorf("MustExecutable: %q, want %q",
|
||||||
|
got, os.Args[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+175
-387
@@ -1,53 +1,38 @@
|
|||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
. "syscall"
|
. "syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
"hakurei.app/container/seccomp"
|
"hakurei.app/container/seccomp"
|
||||||
"hakurei.app/ext"
|
|
||||||
"hakurei.app/fhs"
|
|
||||||
"hakurei.app/internal/params"
|
|
||||||
"hakurei.app/message"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
/* intermediateHostPath is the pathname of the intermediate tmpfs mount point.
|
/* intermediate tmpfs mount point
|
||||||
|
|
||||||
This path might seem like a weird choice, however there are many good reasons to use it:
|
this path might seem like a weird choice, however there are many good reasons to use it:
|
||||||
- The contents of this path is never exposed to the container:
|
- the contents of this path is never exposed to the container:
|
||||||
The tmpfs root established here effectively becomes anonymous after pivot_root.
|
the tmpfs root established here effectively becomes anonymous after pivot_root
|
||||||
- It is safe to assume this path exists and is a directory:
|
- it is safe to assume this path exists and is a directory:
|
||||||
This program will not work correctly without a proper /proc and neither will most others.
|
this program will not work correctly without a proper /proc and neither will most others
|
||||||
- This path belongs to the container init:
|
- this path belongs to the container init:
|
||||||
The container init is not any more privileged or trusted than the rest of the container.
|
the container init is not any more privileged or trusted than the rest of the container
|
||||||
- This path is only accessible by init and root:
|
- this path is only accessible by init and root:
|
||||||
The container init sets SUID_DUMP_DISABLE and terminates if that fails.
|
the container init sets SUID_DUMP_DISABLE and terminates if that fails;
|
||||||
|
|
||||||
It should be noted that none of this should become relevant at any point
|
it should be noted that none of this should become relevant at any point since the resulting
|
||||||
since the resulting intermediate root tmpfs should be effectively anonymous. */
|
intermediate root tmpfs should be effectively anonymous */
|
||||||
intermediateHostPath = fhs.Proc + "self/fd"
|
intermediateHostPath = FHSProc + "self/fd"
|
||||||
|
|
||||||
// setupEnv is the name of the environment variable holding the string
|
// setup params file descriptor
|
||||||
// representation of the read end file descriptor of the setup params pipe.
|
|
||||||
setupEnv = "HAKUREI_SETUP"
|
setupEnv = "HAKUREI_SETUP"
|
||||||
|
|
||||||
// exitUnexpectedWait4 is the exit code if wait4 returns an unexpected errno.
|
|
||||||
exitUnexpectedWait4 = 2
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -61,13 +46,8 @@ type (
|
|||||||
early(state *setupState, k syscallDispatcher) error
|
early(state *setupState, k syscallDispatcher) error
|
||||||
// apply is called in intermediate root.
|
// apply is called in intermediate root.
|
||||||
apply(state *setupState, k syscallDispatcher) error
|
apply(state *setupState, k syscallDispatcher) error
|
||||||
// late is called right before starting the initial process.
|
|
||||||
late(state *setupState, k syscallDispatcher) error
|
|
||||||
|
|
||||||
// prefix returns a log message prefix, and whether this Op prints no
|
|
||||||
// identifying message on its own.
|
|
||||||
prefix() (string, bool)
|
|
||||||
|
|
||||||
|
prefix() string
|
||||||
Is(op Op) bool
|
Is(op Op) bool
|
||||||
Valid() bool
|
Valid() bool
|
||||||
fmt.Stringer
|
fmt.Stringer
|
||||||
@@ -76,31 +56,10 @@ type (
|
|||||||
// setupState persists context between Ops.
|
// setupState persists context between Ops.
|
||||||
setupState struct {
|
setupState struct {
|
||||||
nonrepeatable uintptr
|
nonrepeatable uintptr
|
||||||
|
|
||||||
// Whether early reaping has concluded. Must only be accessed in the
|
|
||||||
// wait4 loop.
|
|
||||||
processConcluded bool
|
|
||||||
// Process to syscall.WaitStatus populated in the wait4 loop. Freed
|
|
||||||
// after early reaping concludes.
|
|
||||||
process map[int]WaitStatus
|
|
||||||
// Synchronises access to process.
|
|
||||||
processMu sync.RWMutex
|
|
||||||
|
|
||||||
*Params
|
*Params
|
||||||
context.Context
|
|
||||||
message.Msg
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// terminated returns whether the specified pid has been reaped, and its
|
|
||||||
// syscall.WaitStatus if it had. This is only usable by [Op].
|
|
||||||
func (state *setupState) terminated(pid int) (wstatus WaitStatus, ok bool) {
|
|
||||||
state.processMu.RLock()
|
|
||||||
wstatus, ok = state.process[pid]
|
|
||||||
state.processMu.RUnlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grow grows the slice Ops points to using [slices.Grow].
|
// Grow grows the slice Ops points to using [slices.Grow].
|
||||||
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
|
func (f *Ops) Grow(n int) { *f = slices.Grow(*f, n) }
|
||||||
|
|
||||||
@@ -130,347 +89,267 @@ type initParams struct {
|
|||||||
Verbose bool
|
Verbose bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init is called by [TryArgv0] if the current process is the container init.
|
func Init(prepareLogger func(prefix string), setVerbose func(verbose bool)) {
|
||||||
func Init(msg message.Msg) { initEntrypoint(direct{}, msg) }
|
initEntrypoint(direct{}, prepareLogger, setVerbose)
|
||||||
|
}
|
||||||
|
|
||||||
func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
func initEntrypoint(k syscallDispatcher, prepareLogger func(prefix string), setVerbose func(verbose bool)) {
|
||||||
k.lockOSThread()
|
k.lockOSThread()
|
||||||
|
prepareLogger("init")
|
||||||
if msg == nil {
|
|
||||||
panic("attempting to call initEntrypoint with nil msg")
|
|
||||||
}
|
|
||||||
|
|
||||||
if k.getpid() != 1 {
|
if k.getpid() != 1 {
|
||||||
k.fatal(msg, "this process must run as pid 1")
|
k.fatal("this process must run as pid 1")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := k.setPtracer(0); err != nil {
|
if err := k.setPtracer(0); err != nil {
|
||||||
msg.Verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
|
k.verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
|
||||||
// not fatal: this program has no additional privileges at initial program start
|
// not fatal: this program has no additional privileges at initial program start
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
param initParams
|
params initParams
|
||||||
closeSetup func() error
|
closeSetup func() error
|
||||||
setupFd int
|
setupFd uintptr
|
||||||
|
offsetSetup int
|
||||||
)
|
)
|
||||||
if f, err := k.receive(setupEnv, ¶m, &setupFd); err != nil {
|
if f, err := k.receive(setupEnv, ¶ms, &setupFd); err != nil {
|
||||||
if errors.Is(err, EBADF) {
|
if errors.Is(err, EBADF) {
|
||||||
k.fatal(msg, "invalid setup descriptor")
|
k.fatal("invalid setup descriptor")
|
||||||
}
|
}
|
||||||
if errors.Is(err, params.ErrReceiveEnv) {
|
if errors.Is(err, ErrReceiveEnv) {
|
||||||
k.fatal(msg, setupEnv+" not set")
|
k.fatal("HAKUREI_SETUP not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
k.fatalf(msg, "cannot decode init setup payload: %v", err)
|
k.fatalf("cannot decode init setup payload: %v", err)
|
||||||
} else {
|
} else {
|
||||||
if param.Ops == nil {
|
if params.Ops == nil {
|
||||||
k.fatal(msg, "invalid setup parameters")
|
k.fatal("invalid setup parameters")
|
||||||
}
|
}
|
||||||
if param.ParentPerm == 0 {
|
if params.ParentPerm == 0 {
|
||||||
param.ParentPerm = 0755
|
params.ParentPerm = 0755
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.SwapVerbose(param.Verbose)
|
setVerbose(params.Verbose)
|
||||||
msg.Verbose("received setup parameters")
|
k.verbose("received setup parameters")
|
||||||
closeSetup = f
|
closeSetup = f
|
||||||
}
|
offsetSetup = int(setupFd + 1)
|
||||||
|
|
||||||
if !param.HostNet {
|
|
||||||
ctx, cancel := signal.NotifyContext(context.Background(), CancelSignal,
|
|
||||||
os.Interrupt, SIGTERM, SIGQUIT)
|
|
||||||
defer cancel() // for panics
|
|
||||||
k.mustLoopback(ctx, msg)
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
uid, gid := param.Uid, param.Gid
|
|
||||||
if param.InitAsRoot {
|
|
||||||
uid, gid = 0, 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// write uid/gid map here so parent does not need to set dumpable
|
// write uid/gid map here so parent does not need to set dumpable
|
||||||
if err := k.setDumpable(ext.SUID_DUMP_USER); err != nil {
|
if err := k.setDumpable(SUID_DUMP_USER); err != nil {
|
||||||
k.fatalf(msg, "cannot set SUID_DUMP_USER: %v", err)
|
k.fatalf("cannot set SUID_DUMP_USER: %v", err)
|
||||||
}
|
}
|
||||||
if err := k.writeFile(
|
if err := k.writeFile(FHSProc+"self/uid_map",
|
||||||
fhs.Proc+"self/uid_map",
|
append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
|
||||||
[]byte(strconv.Itoa(uid)+" "+strconv.Itoa(param.HostUid)+" 1\n"),
|
0); err != nil {
|
||||||
0,
|
k.fatalf("%v", err)
|
||||||
); err != nil {
|
|
||||||
k.fatalf(msg, "%v", err)
|
|
||||||
}
|
}
|
||||||
if err := k.writeFile(
|
if err := k.writeFile(FHSProc+"self/setgroups",
|
||||||
fhs.Proc+"self/setgroups",
|
|
||||||
[]byte("deny\n"),
|
[]byte("deny\n"),
|
||||||
0,
|
0); err != nil && !os.IsNotExist(err) {
|
||||||
); err != nil && !os.IsNotExist(err) {
|
k.fatalf("%v", err)
|
||||||
k.fatalf(msg, "%v", err)
|
|
||||||
}
|
}
|
||||||
if err := k.writeFile(fhs.Proc+"self/gid_map",
|
if err := k.writeFile(FHSProc+"self/gid_map",
|
||||||
[]byte(strconv.Itoa(gid)+" "+strconv.Itoa(param.HostGid)+" 1\n"),
|
append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
|
||||||
0,
|
0); err != nil {
|
||||||
); err != nil {
|
k.fatalf("%v", err)
|
||||||
k.fatalf(msg, "%v", err)
|
|
||||||
}
|
}
|
||||||
if err := k.setDumpable(ext.SUID_DUMP_DISABLE); err != nil {
|
if err := k.setDumpable(SUID_DUMP_DISABLE); err != nil {
|
||||||
k.fatalf(msg, "cannot set SUID_DUMP_DISABLE: %v", err)
|
k.fatalf("cannot set SUID_DUMP_DISABLE: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
oldmask := k.umask(0)
|
oldmask := k.umask(0)
|
||||||
if param.Hostname != "" {
|
if params.Hostname != "" {
|
||||||
if err := k.sethostname([]byte(param.Hostname)); err != nil {
|
if err := k.sethostname([]byte(params.Hostname)); err != nil {
|
||||||
k.fatalf(msg, "cannot set hostname: %v", err)
|
k.fatalf("cannot set hostname: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cache sysctl before pivot_root
|
// cache sysctl before pivot_root
|
||||||
lastcap := k.lastcap(msg)
|
lastcap := k.lastcap()
|
||||||
|
|
||||||
if err := k.mount(zeroString, fhs.Root, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil {
|
if err := k.mount(zeroString, FHSRoot, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil {
|
||||||
k.fatalf(msg, "cannot make / rslave: %v", optionalErrorUnwrap(err))
|
k.fatalf("cannot make / rslave: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
state := &setupState{Params: ¶ms.Params}
|
||||||
state := &setupState{process: make(map[int]WaitStatus), Params: ¶m.Params, Msg: msg, Context: ctx}
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := k.mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil {
|
|
||||||
k.fatalf(msg, "cannot mount intermediate root: %v", optionalErrorUnwrap(err))
|
|
||||||
}
|
|
||||||
if err := k.chdir(intermediateHostPath); err != nil {
|
|
||||||
k.fatalf(msg, "cannot enter intermediate host path: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(param.Binfmt) > 0 {
|
|
||||||
for i, e := range param.Binfmt {
|
|
||||||
if pathname, err := k.evalSymlinks(e.Interpreter.String()); err != nil {
|
|
||||||
k.fatal(msg, err)
|
|
||||||
} else if param.Binfmt[i].Interpreter, err = check.NewAbs(pathname); err != nil {
|
|
||||||
k.fatal(msg, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* early is called right before pivot_root into intermediate root;
|
/* early is called right before pivot_root into intermediate root;
|
||||||
this step is mostly for gathering information that would otherwise be
|
this step is mostly for gathering information that would otherwise be difficult to obtain
|
||||||
difficult to obtain via library functions after pivot_root, and
|
via library functions after pivot_root, and implementations are expected to avoid changing
|
||||||
implementations are expected to avoid changing the state of the mount
|
the state of the mount namespace */
|
||||||
namespace */
|
for i, op := range *params.Ops {
|
||||||
for i, op := range *param.Ops {
|
|
||||||
if op == nil || !op.Valid() {
|
if op == nil || !op.Valid() {
|
||||||
k.fatalf(msg, "invalid op at index %d", i)
|
k.fatalf("invalid op at index %d", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := op.early(state, k); err != nil {
|
if err := op.early(state, k); err != nil {
|
||||||
if m, ok := messageFromError(err); ok {
|
if m, ok := messageFromError(err); ok {
|
||||||
k.fatal(msg, m)
|
k.fatal(m)
|
||||||
} else {
|
} else {
|
||||||
k.fatalf(msg, "cannot prepare op at index %d: %v", i, err)
|
k.fatalf("cannot prepare op at index %d: %v", i, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := k.mount(SourceTmpfsRootfs, intermediateHostPath, FstypeTmpfs, MS_NODEV|MS_NOSUID, zeroString); err != nil {
|
||||||
|
k.fatalf("cannot mount intermediate root: %v", err)
|
||||||
|
}
|
||||||
|
if err := k.chdir(intermediateHostPath); err != nil {
|
||||||
|
k.fatalf("cannot enter intermediate host path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := k.mkdir(sysrootDir, 0755); err != nil {
|
if err := k.mkdir(sysrootDir, 0755); err != nil {
|
||||||
k.fatalf(msg, "%v", err)
|
k.fatalf("%v", err)
|
||||||
}
|
}
|
||||||
if err := k.mount(sysrootDir, sysrootDir, zeroString, MS_SILENT|MS_BIND|MS_REC, zeroString); err != nil {
|
if err := k.mount(sysrootDir, sysrootDir, zeroString, MS_SILENT|MS_BIND|MS_REC, zeroString); err != nil {
|
||||||
k.fatalf(msg, "cannot bind sysroot: %v", optionalErrorUnwrap(err))
|
k.fatalf("cannot bind sysroot: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := k.mkdir(hostDir, 0755); err != nil {
|
if err := k.mkdir(hostDir, 0755); err != nil {
|
||||||
k.fatalf(msg, "%v", err)
|
k.fatalf("%v", err)
|
||||||
}
|
}
|
||||||
// pivot_root uncovers intermediateHostPath in hostDir
|
// pivot_root uncovers intermediateHostPath in hostDir
|
||||||
if err := k.pivotRoot(intermediateHostPath, hostDir); err != nil {
|
if err := k.pivotRoot(intermediateHostPath, hostDir); err != nil {
|
||||||
k.fatalf(msg, "cannot pivot into intermediate root: %v", err)
|
k.fatalf("cannot pivot into intermediate root: %v", err)
|
||||||
}
|
}
|
||||||
if err := k.chdir(fhs.Root); err != nil {
|
if err := k.chdir(FHSRoot); err != nil {
|
||||||
k.fatalf(msg, "cannot enter intermediate root: %v", err)
|
k.fatalf("cannot enter intermediate root: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* apply is called right after pivot_root and entering the new root. This
|
/* apply is called right after pivot_root and entering the new root;
|
||||||
step sets up the container filesystem, and implementations are expected to
|
this step sets up the container filesystem, and implementations are expected to keep the host root
|
||||||
keep the host root and sysroot mount points intact but otherwise can do
|
and sysroot mount points intact but otherwise can do whatever they need to;
|
||||||
whatever they need to. Calling chdir is allowed but discouraged. */
|
chdir is allowed but discouraged */
|
||||||
for i, op := range *param.Ops {
|
for i, op := range *params.Ops {
|
||||||
// ops already checked during early setup
|
// ops already checked during early setup
|
||||||
if prefix, ok := op.prefix(); ok {
|
k.verbosef("%s %s", op.prefix(), op)
|
||||||
msg.Verbosef("%s %s", prefix, op)
|
|
||||||
}
|
|
||||||
if err := op.apply(state, k); err != nil {
|
if err := op.apply(state, k); err != nil {
|
||||||
if m, ok := messageFromError(err); ok {
|
if m, ok := messageFromError(err); ok {
|
||||||
k.fatal(msg, m)
|
k.fatal(m)
|
||||||
} else {
|
} else {
|
||||||
k.fatalf(msg, "cannot apply op at index %d: %v", i, err)
|
k.fatalf("cannot apply op at index %d: %v", i, err)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(param.Binfmt) > 0 {
|
|
||||||
const interpreter = "/interpreter"
|
|
||||||
|
|
||||||
if param.BinfmtPath == nil {
|
|
||||||
param.BinfmtPath = fhs.AbsProcSys.Append("fs/binfmt_misc")
|
|
||||||
}
|
|
||||||
binfmt := sysrootPath + param.BinfmtPath.String()
|
|
||||||
if err := k.mkdirAll(binfmt, 0); err != nil {
|
|
||||||
k.fatal(msg, err)
|
|
||||||
}
|
|
||||||
if err := k.mount(
|
|
||||||
SourceBinfmtMisc,
|
|
||||||
binfmt,
|
|
||||||
FstypeBinfmtMisc,
|
|
||||||
MS_NOSUID|MS_NOEXEC|MS_NODEV,
|
|
||||||
zeroString,
|
|
||||||
); err != nil {
|
|
||||||
k.fatal(msg, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf strings.Builder
|
|
||||||
buf.Grow(1920)
|
|
||||||
|
|
||||||
register := binfmt + "/register"
|
|
||||||
for i, e := range param.Binfmt {
|
|
||||||
if err := k.symlink(hostPath+e.Interpreter.String(), interpreter); err != nil {
|
|
||||||
k.fatal(msg, err)
|
|
||||||
} else if err = k.writeFile(register, []byte(":"+
|
|
||||||
strconv.Itoa(i)+":"+
|
|
||||||
"M:"+
|
|
||||||
strconv.Itoa(int(e.Offset))+":"+
|
|
||||||
escapeBinfmt(&buf, e.Magic)+":"+
|
|
||||||
escapeBinfmt(&buf, e.Mask)+":"+
|
|
||||||
interpreter+":"+
|
|
||||||
"F"), 0); err != nil {
|
|
||||||
k.fatal(msg, err)
|
|
||||||
} else if err = k.remove(interpreter); err != nil {
|
|
||||||
k.fatal(msg, err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup requiring host root complete at this point
|
// setup requiring host root complete at this point
|
||||||
if err := k.mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil {
|
if err := k.mount(hostDir, hostDir, zeroString, MS_SILENT|MS_REC|MS_PRIVATE, zeroString); err != nil {
|
||||||
k.fatalf(msg, "cannot make host root rprivate: %v", optionalErrorUnwrap(err))
|
k.fatalf("cannot make host root rprivate: %v", err)
|
||||||
}
|
}
|
||||||
if err := k.unmount(hostDir, MNT_DETACH); err != nil {
|
if err := k.unmount(hostDir, MNT_DETACH); err != nil {
|
||||||
k.fatalf(msg, "cannot unmount host root: %v", err)
|
k.fatalf("cannot unmount host root: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
var fd int
|
var fd int
|
||||||
if err := ext.IgnoringEINTR(func() (err error) {
|
if err := IgnoringEINTR(func() (err error) {
|
||||||
fd, err = k.open(fhs.Root, O_DIRECTORY|O_RDONLY, 0)
|
fd, err = k.open(FHSRoot, O_DIRECTORY|O_RDONLY, 0)
|
||||||
return
|
return
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
k.fatalf(msg, "cannot open intermediate root: %v", err)
|
k.fatalf("cannot open intermediate root: %v", err)
|
||||||
}
|
}
|
||||||
if err := k.chdir(sysrootPath); err != nil {
|
if err := k.chdir(sysrootPath); err != nil {
|
||||||
k.fatalf(msg, "cannot enter sysroot: %v", err)
|
k.fatalf("cannot enter sysroot: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := k.pivotRoot(".", "."); err != nil {
|
if err := k.pivotRoot(".", "."); err != nil {
|
||||||
k.fatalf(msg, "cannot pivot into sysroot: %v", err)
|
k.fatalf("cannot pivot into sysroot: %v", err)
|
||||||
}
|
}
|
||||||
if err := k.fchdir(fd); err != nil {
|
if err := k.fchdir(fd); err != nil {
|
||||||
k.fatalf(msg, "cannot re-enter intermediate root: %v", err)
|
k.fatalf("cannot re-enter intermediate root: %v", err)
|
||||||
}
|
}
|
||||||
if err := k.unmount(".", MNT_DETACH); err != nil {
|
if err := k.unmount(".", MNT_DETACH); err != nil {
|
||||||
k.fatalf(msg, "cannot unmount intermediate root: %v", err)
|
k.fatalf("cannot unmount intermediate root: %v", err)
|
||||||
}
|
}
|
||||||
if err := k.chdir(fhs.Root); err != nil {
|
if err := k.chdir(FHSRoot); err != nil {
|
||||||
k.fatalf(msg, "cannot enter root: %v", err)
|
k.fatalf("cannot enter root: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := k.close(fd); err != nil {
|
if err := k.close(fd); err != nil {
|
||||||
k.fatalf(msg, "cannot close intermediate root: %v", err)
|
k.fatalf("cannot close intermediate root: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var keepCaps []uintptr
|
|
||||||
if param.Privileged {
|
|
||||||
keepCaps = append(keepCaps, CAP_SYS_ADMIN, CAP_SETPCAP)
|
|
||||||
}
|
|
||||||
if param.InitAsRoot {
|
|
||||||
keepCaps = append(keepCaps, CAP_SETFCAP)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := k.capAmbientClearAll(); err != nil {
|
if err := k.capAmbientClearAll(); err != nil {
|
||||||
k.fatalf(msg, "cannot clear the ambient capability set: %v", err)
|
k.fatalf("cannot clear the ambient capability set: %v", err)
|
||||||
}
|
}
|
||||||
for i := range lastcap + 1 {
|
for i := uintptr(0); i <= lastcap; i++ {
|
||||||
if slices.Contains(keepCaps, i) {
|
if params.Privileged && i == CAP_SYS_ADMIN {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := k.capBoundingSetDrop(i); err != nil {
|
if err := k.capBoundingSetDrop(i); err != nil {
|
||||||
k.fatalf(msg, "cannot drop capability from bounding set: %v", err)
|
k.fatalf("cannot drop capability from bounding set: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var keep [2]uint32
|
var keep [2]uint32
|
||||||
for _, c := range keepCaps {
|
if params.Privileged {
|
||||||
keep[capToIndex(c)] |= capToMask(c)
|
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
|
||||||
}
|
|
||||||
|
|
||||||
|
if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil {
|
||||||
|
k.fatalf("cannot raise CAP_SYS_ADMIN: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := k.capset(
|
if err := k.capset(
|
||||||
&capHeader{_LINUX_CAPABILITY_VERSION_3, 0},
|
&capHeader{_LINUX_CAPABILITY_VERSION_3, 0},
|
||||||
&[2]capData{{keep[0], keep[0], keep[0]}, {keep[1], keep[1], keep[1]}},
|
&[2]capData{{0, keep[0], keep[0]}, {0, keep[1], keep[1]}},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
k.fatalf(msg, "cannot capset: %v", err)
|
k.fatalf("cannot capset: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range keepCaps {
|
if !params.SeccompDisable {
|
||||||
if err := k.capAmbientRaise(c); err != nil {
|
rules := params.SeccompRules
|
||||||
k.fatalf(msg, "cannot raise %#x: %v", c, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !param.SeccompDisable {
|
|
||||||
rules := param.SeccompRules
|
|
||||||
if len(rules) == 0 { // non-empty rules slice always overrides presets
|
if len(rules) == 0 { // non-empty rules slice always overrides presets
|
||||||
msg.Verbosef("resolving presets %#x", param.SeccompPresets)
|
k.verbosef("resolving presets %#x", params.SeccompPresets)
|
||||||
rules = seccomp.Preset(param.SeccompPresets, param.SeccompFlags)
|
rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags)
|
||||||
}
|
}
|
||||||
if err := k.seccompLoad(rules, param.SeccompFlags); err != nil {
|
if err := k.seccompLoad(rules, params.SeccompFlags); err != nil {
|
||||||
// this also indirectly asserts PR_SET_NO_NEW_PRIVS
|
// this also indirectly asserts PR_SET_NO_NEW_PRIVS
|
||||||
k.fatalf(msg, "cannot load syscall filter: %v", err)
|
k.fatalf("cannot load syscall filter: %v", err)
|
||||||
}
|
}
|
||||||
msg.Verbosef("%d filter rules loaded", len(rules))
|
k.verbosef("%d filter rules loaded", len(rules))
|
||||||
} else {
|
} else {
|
||||||
msg.Verbose("syscall filter not configured")
|
k.verbose("syscall filter not configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
extraFiles := make([]*os.File, param.Count)
|
extraFiles := make([]*os.File, params.Count)
|
||||||
for i := range extraFiles {
|
for i := range extraFiles {
|
||||||
// setup fd is placed before all extra files
|
// setup fd is placed before all extra files
|
||||||
extraFiles[i] = k.newFile(uintptr(setupFd+1+i), "extra file "+strconv.Itoa(i))
|
extraFiles[i] = k.newFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
|
||||||
}
|
}
|
||||||
k.umask(oldmask)
|
k.umask(oldmask)
|
||||||
|
|
||||||
// winfo represents an exited process from wait4.
|
cmd := exec.Command(params.Path.String())
|
||||||
|
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||||
|
cmd.Args = params.Args
|
||||||
|
cmd.Env = params.Env
|
||||||
|
cmd.ExtraFiles = extraFiles
|
||||||
|
cmd.Dir = params.Dir.String()
|
||||||
|
|
||||||
|
k.verbosef("starting initial program %s", params.Path)
|
||||||
|
if err := k.start(cmd); err != nil {
|
||||||
|
k.fatalf("%v", err)
|
||||||
|
}
|
||||||
|
k.suspend()
|
||||||
|
|
||||||
|
if err := closeSetup(); err != nil {
|
||||||
|
k.printf("cannot close setup pipe: %v", err)
|
||||||
|
// not fatal
|
||||||
|
}
|
||||||
|
|
||||||
type winfo struct {
|
type winfo struct {
|
||||||
wpid int
|
wpid int
|
||||||
wstatus WaitStatus
|
wstatus WaitStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
// info is closed as the wait4 thread terminates
|
|
||||||
// when there are no longer any processes left to reap
|
|
||||||
info := make(chan winfo, 1)
|
info := make(chan winfo, 1)
|
||||||
|
done := make(chan struct{})
|
||||||
// whether initial process has started
|
|
||||||
var initialProcessStarted atomic.Bool
|
|
||||||
|
|
||||||
k.new(func(k syscallDispatcher) {
|
k.new(func(k syscallDispatcher) {
|
||||||
k.lockOSThread()
|
|
||||||
|
|
||||||
wait4:
|
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
wpid = -2
|
wpid = -2
|
||||||
wstatus WaitStatus
|
wstatus WaitStatus
|
||||||
|
|
||||||
// whether initial process has started
|
|
||||||
started bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// keep going until no child process is left
|
// keep going until no child process is left
|
||||||
@@ -480,25 +359,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if wpid != -2 {
|
if wpid != -2 {
|
||||||
if !state.processConcluded {
|
info <- winfo{wpid, wstatus}
|
||||||
state.processMu.Lock()
|
|
||||||
if state.process == nil {
|
|
||||||
// early reaping has already concluded at this point
|
|
||||||
state.processConcluded = true
|
|
||||||
info <- winfo{wpid, wstatus}
|
|
||||||
} else {
|
|
||||||
// initial process has not yet been created, and the
|
|
||||||
// info channel is not yet being received from
|
|
||||||
state.process[wpid] = wstatus
|
|
||||||
}
|
|
||||||
state.processMu.Unlock()
|
|
||||||
} else {
|
|
||||||
info <- winfo{wpid, wstatus}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !started {
|
|
||||||
started = initialProcessStarted.Load()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = EINTR
|
err = EINTR
|
||||||
@@ -506,153 +367,80 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
|||||||
wpid, err = k.wait4(-1, &wstatus, 0, nil)
|
wpid, err = k.wait4(-1, &wstatus, 0, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !errors.Is(err, ECHILD) {
|
if !errors.Is(err, ECHILD) {
|
||||||
k.printf(msg, "unexpected wait4 response: %v", err)
|
k.printf("unexpected wait4 response: %v", err)
|
||||||
} else if !started {
|
|
||||||
// initial process has not yet been reached and all daemons
|
|
||||||
// terminated or none were started in the first place
|
|
||||||
time.Sleep(500 * time.Microsecond)
|
|
||||||
goto wait4
|
|
||||||
}
|
}
|
||||||
|
|
||||||
close(info)
|
close(done)
|
||||||
})
|
})
|
||||||
|
|
||||||
// called right before startup of initial process, all state changes to the
|
|
||||||
// current process is prohibited during late
|
|
||||||
for i, op := range *param.Ops {
|
|
||||||
// ops already checked during early setup
|
|
||||||
if err := op.late(state, k); err != nil {
|
|
||||||
if m, ok := messageFromError(err); ok {
|
|
||||||
k.fatal(msg, m)
|
|
||||||
} else if errors.Is(err, context.DeadlineExceeded) {
|
|
||||||
k.fatalf(msg, "%s deadline exceeded", op.String())
|
|
||||||
} else {
|
|
||||||
k.fatalf(msg, "cannot complete op at index %d: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// early reaping has concluded, this must happen before initial process is created
|
|
||||||
state.processMu.Lock()
|
|
||||||
state.process = nil
|
|
||||||
state.processMu.Unlock()
|
|
||||||
|
|
||||||
if err := closeSetup(); err != nil {
|
|
||||||
k.fatalf(msg, "cannot close setup pipe: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(param.Path.String())
|
|
||||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
|
||||||
cmd.Args = param.Args
|
|
||||||
cmd.Env = param.Env
|
|
||||||
cmd.ExtraFiles = extraFiles
|
|
||||||
cmd.Dir = param.Dir.String()
|
|
||||||
|
|
||||||
if param.InitAsRoot {
|
|
||||||
cmd.SysProcAttr = &SysProcAttr{
|
|
||||||
Cloneflags: CLONE_NEWUSER,
|
|
||||||
UidMappings: []SysProcIDMap{{ContainerID: param.Uid, HostID: 0, Size: 1}},
|
|
||||||
GidMappings: []SysProcIDMap{{ContainerID: param.Gid, HostID: 0, Size: 1}},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
msg.Verbosef("starting initial process %s", param.Path)
|
|
||||||
if err := k.start(cmd); err != nil {
|
|
||||||
k.fatalf(msg, "%v", err)
|
|
||||||
}
|
|
||||||
initialProcessStarted.Store(true)
|
|
||||||
|
|
||||||
// handle signals to dump withheld messages
|
// handle signals to dump withheld messages
|
||||||
sig := make(chan os.Signal, 2)
|
sig := make(chan os.Signal, 2)
|
||||||
k.notify(sig, CancelSignal,
|
k.notify(sig, os.Interrupt, CancelSignal)
|
||||||
os.Interrupt, SIGTERM, SIGQUIT)
|
|
||||||
|
|
||||||
// closed after residualProcessTimeout has elapsed after initial process death
|
// closed after residualProcessTimeout has elapsed after initial process death
|
||||||
timeout := make(chan struct{})
|
timeout := make(chan struct{})
|
||||||
|
|
||||||
r := exitUnexpectedWait4
|
r := 2
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case s := <-sig:
|
case s := <-sig:
|
||||||
if s == CancelSignal && param.ForwardCancel && cmd.Process != nil {
|
if k.resume() {
|
||||||
msg.Verbose("forwarding context cancellation")
|
k.verbosef("%s after process start", s.String())
|
||||||
if err := k.signal(cmd, os.Interrupt); err != nil && !errors.Is(err, os.ErrProcessDone) {
|
} else {
|
||||||
k.printf(msg, "cannot forward cancellation: %v", err)
|
k.verbosef("got %s", s.String())
|
||||||
|
}
|
||||||
|
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
|
||||||
|
k.verbose("forwarding context cancellation")
|
||||||
|
if err := k.signal(cmd, os.Interrupt); err != nil {
|
||||||
|
k.printf("cannot forward cancellation: %v", err)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
k.beforeExit()
|
||||||
if s == SIGTERM || s == SIGQUIT {
|
|
||||||
msg.Verbosef("got %s, forwarding to initial process", s.String())
|
|
||||||
if err := k.signal(cmd, s); err != nil {
|
|
||||||
k.printf(msg, "cannot forward signal: %v", err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
msg.Verbosef("got %s", s.String())
|
|
||||||
msg.BeforeExit()
|
|
||||||
k.exit(0)
|
k.exit(0)
|
||||||
|
|
||||||
case w, ok := <-info:
|
case w := <-info:
|
||||||
if !ok {
|
|
||||||
msg.BeforeExit()
|
|
||||||
k.exit(r)
|
|
||||||
continue // unreachable
|
|
||||||
}
|
|
||||||
|
|
||||||
if w.wpid == cmd.Process.Pid {
|
if w.wpid == cmd.Process.Pid {
|
||||||
// cancel Op context early
|
// initial process exited, output is most likely available again
|
||||||
cancel()
|
k.resume()
|
||||||
|
|
||||||
// start timeout early
|
|
||||||
go func() { time.Sleep(param.AdoptWaitDelay); close(timeout) }()
|
|
||||||
|
|
||||||
// close initial process files; this also keeps them alive
|
|
||||||
for _, f := range extraFiles {
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
msg.Verbose(err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case w.wstatus.Exited():
|
case w.wstatus.Exited():
|
||||||
r = w.wstatus.ExitStatus()
|
r = w.wstatus.ExitStatus()
|
||||||
msg.Verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
|
k.verbosef("initial process exited with code %d", w.wstatus.ExitStatus())
|
||||||
|
|
||||||
case w.wstatus.Signaled():
|
case w.wstatus.Signaled():
|
||||||
r = 128 + int(w.wstatus.Signal())
|
r = 128 + int(w.wstatus.Signal())
|
||||||
msg.Verbosef("initial process exited with signal %s", w.wstatus.Signal())
|
k.verbosef("initial process exited with signal %s", w.wstatus.Signal())
|
||||||
|
|
||||||
default:
|
default:
|
||||||
r = 255
|
r = 255
|
||||||
msg.Verbosef("initial process exited with status %#x", w.wstatus)
|
k.verbosef("initial process exited with status %#x", w.wstatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case <-done:
|
||||||
|
k.beforeExit()
|
||||||
|
k.exit(r)
|
||||||
|
|
||||||
case <-timeout:
|
case <-timeout:
|
||||||
k.printf(msg, "timeout exceeded waiting for lingering processes")
|
k.printf("timeout exceeded waiting for lingering processes")
|
||||||
msg.BeforeExit()
|
k.beforeExit()
|
||||||
k.exit(r)
|
k.exit(r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// initName is the prefix used by log.std in the init process.
|
|
||||||
const initName = "init"
|
const initName = "init"
|
||||||
|
|
||||||
// TryArgv0 calls [Init] if the last element of argv0 is "init".
|
// TryArgv0 calls [Init] if the last element of argv0 is "init".
|
||||||
// If a nil msg is passed, the system logger is used instead.
|
func TryArgv0(v Msg, prepare func(prefix string), setVerbose func(verbose bool)) {
|
||||||
func TryArgv0(msg message.Msg) {
|
if len(os.Args) > 0 && path.Base(os.Args[0]) == initName {
|
||||||
if msg == nil {
|
msg = v
|
||||||
log.SetPrefix(initName + ": ")
|
Init(prepare, setVerbose)
|
||||||
log.SetFlags(0)
|
|
||||||
msg = message.New(log.Default())
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(os.Args) > 0 && filepath.Base(os.Args[0]) == initName {
|
|
||||||
Init(msg)
|
|
||||||
msg.BeforeExit()
|
msg.BeforeExit()
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|||||||
+634
-784
File diff suppressed because it is too large
Load Diff
+26
-26
@@ -5,57 +5,63 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
"hakurei.app/container/std"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() { gob.Register(new(BindMountOp)) }
|
func init() { gob.Register(new(BindMountOp)) }
|
||||||
|
|
||||||
// Bind is a helper for appending [BindMountOp] to [Ops].
|
// Bind appends an [Op] that bind mounts host path [BindMountOp.Source] on container path [BindMountOp.Target].
|
||||||
func (f *Ops) Bind(source, target *check.Absolute, flags int) *Ops {
|
func (f *Ops) Bind(source, target *Absolute, flags int) *Ops {
|
||||||
*f = append(*f, &BindMountOp{nil, source, target, flags})
|
*f = append(*f, &BindMountOp{nil, source, target, flags})
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
// BindMountOp creates a bind mount from host path Source to container path Target.
|
// BindMountOp bind mounts host path Source on container path Target.
|
||||||
//
|
// Note that Flags uses bits declared in this package and should not be set with constants in [syscall].
|
||||||
// Note that Flags uses bits declared in the [std] package and should not be set
|
|
||||||
// with constants in [syscall].
|
|
||||||
type BindMountOp struct {
|
type BindMountOp struct {
|
||||||
sourceFinal, Source, Target *check.Absolute
|
sourceFinal, Source, Target *Absolute
|
||||||
|
|
||||||
Flags int
|
Flags int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// BindOptional skips nonexistent host paths.
|
||||||
|
BindOptional = 1 << iota
|
||||||
|
// BindWritable mounts filesystem read-write.
|
||||||
|
BindWritable
|
||||||
|
// BindDevice allows access to devices (special files) on this filesystem.
|
||||||
|
BindDevice
|
||||||
|
// BindEnsure attempts to create the host path if it does not exist.
|
||||||
|
BindEnsure
|
||||||
|
)
|
||||||
|
|
||||||
func (b *BindMountOp) Valid() bool {
|
func (b *BindMountOp) Valid() bool {
|
||||||
return b != nil &&
|
return b != nil &&
|
||||||
b.Source != nil && b.Target != nil &&
|
b.Source != nil && b.Target != nil &&
|
||||||
b.Flags&(std.BindOptional|std.BindEnsure) != (std.BindOptional|std.BindEnsure)
|
b.Flags&(BindOptional|BindEnsure) != (BindOptional|BindEnsure)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BindMountOp) early(_ *setupState, k syscallDispatcher) error {
|
func (b *BindMountOp) early(_ *setupState, k syscallDispatcher) error {
|
||||||
if b.Flags&std.BindEnsure != 0 {
|
if b.Flags&BindEnsure != 0 {
|
||||||
if err := k.mkdirAll(b.Source.String(), 0700); err != nil {
|
if err := k.mkdirAll(b.Source.String(), 0700); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if pathname, err := k.evalSymlinks(b.Source.String()); err != nil {
|
if pathname, err := k.evalSymlinks(b.Source.String()); err != nil {
|
||||||
if os.IsNotExist(err) && b.Flags&std.BindOptional != 0 {
|
if os.IsNotExist(err) && b.Flags&BindOptional != 0 {
|
||||||
// leave sourceFinal as nil
|
// leave sourceFinal as nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
b.sourceFinal, err = check.NewAbs(pathname)
|
b.sourceFinal, err = NewAbs(pathname)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BindMountOp) apply(state *setupState, k syscallDispatcher) error {
|
func (b *BindMountOp) apply(_ *setupState, k syscallDispatcher) error {
|
||||||
if b.sourceFinal == nil {
|
if b.sourceFinal == nil {
|
||||||
if b.Flags&std.BindOptional == 0 {
|
if b.Flags&BindOptional == 0 {
|
||||||
// unreachable
|
// unreachable
|
||||||
return OpStateError("bind")
|
return OpStateError("bind")
|
||||||
}
|
}
|
||||||
@@ -78,21 +84,15 @@ func (b *BindMountOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var flags uintptr = syscall.MS_REC
|
var flags uintptr = syscall.MS_REC
|
||||||
if b.Flags&std.BindWritable == 0 {
|
if b.Flags&BindWritable == 0 {
|
||||||
flags |= syscall.MS_RDONLY
|
flags |= syscall.MS_RDONLY
|
||||||
}
|
}
|
||||||
if b.Flags&std.BindDevice == 0 {
|
if b.Flags&BindDevice == 0 {
|
||||||
flags |= syscall.MS_NODEV
|
flags |= syscall.MS_NODEV
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.sourceFinal.String() == b.Target.String() {
|
return k.bindMount(source, target, flags, b.sourceFinal == b.Target)
|
||||||
state.Verbosef("mounting %q flags %#x", target, flags)
|
|
||||||
} else {
|
|
||||||
state.Verbosef("mounting %q on %q flags %#x", source, target, flags)
|
|
||||||
}
|
|
||||||
return k.bindMount(state, source, target, flags)
|
|
||||||
}
|
}
|
||||||
func (b *BindMountOp) late(*setupState, syscallDispatcher) error { return nil }
|
|
||||||
|
|
||||||
func (b *BindMountOp) Is(op Op) bool {
|
func (b *BindMountOp) Is(op Op) bool {
|
||||||
vb, ok := op.(*BindMountOp)
|
vb, ok := op.(*BindMountOp)
|
||||||
@@ -101,7 +101,7 @@ func (b *BindMountOp) Is(op Op) bool {
|
|||||||
b.Target.Is(vb.Target) &&
|
b.Target.Is(vb.Target) &&
|
||||||
b.Flags == vb.Flags
|
b.Flags == vb.Flags
|
||||||
}
|
}
|
||||||
func (*BindMountOp) prefix() (string, bool) { return "mounting", false }
|
func (*BindMountOp) prefix() string { return "mounting" }
|
||||||
func (b *BindMountOp) String() string {
|
func (b *BindMountOp) String() string {
|
||||||
if b.Source == nil || b.Target == nil {
|
if b.Source == nil || b.Target == nil {
|
||||||
return "<invalid>"
|
return "<invalid>"
|
||||||
|
|||||||
+69
-94
@@ -6,47 +6,42 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"hakurei.app/check"
|
"hakurei.app/container/stub"
|
||||||
"hakurei.app/container/std"
|
|
||||||
"hakurei.app/internal/stub"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBindMountOp(t *testing.T) {
|
func TestBindMountOp(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||||
{"ENOENT not optional", new(Params), &BindMountOp{
|
{"ENOENT not optional", new(Params), &BindMountOp{
|
||||||
Source: check.MustAbs("/bin/"),
|
Source: MustAbs("/bin/"),
|
||||||
Target: check.MustAbs("/bin/"),
|
Target: MustAbs("/bin/"),
|
||||||
}, []stub.Call{
|
}, []stub.Call{
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT),
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT),
|
||||||
}, syscall.ENOENT, nil, nil},
|
}, syscall.ENOENT, nil, nil},
|
||||||
|
|
||||||
{"skip optional", new(Params), &BindMountOp{
|
{"skip optional", new(Params), &BindMountOp{
|
||||||
Source: check.MustAbs("/bin/"),
|
Source: MustAbs("/bin/"),
|
||||||
Target: check.MustAbs("/bin/"),
|
Target: MustAbs("/bin/"),
|
||||||
Flags: std.BindOptional,
|
Flags: BindOptional,
|
||||||
}, []stub.Call{
|
}, []stub.Call{
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT),
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "", syscall.ENOENT),
|
||||||
}, nil, nil, nil},
|
}, nil, nil, nil},
|
||||||
|
|
||||||
{"success optional", new(Params), &BindMountOp{
|
{"success optional", new(Params), &BindMountOp{
|
||||||
Source: check.MustAbs("/bin/"),
|
Source: MustAbs("/bin/"),
|
||||||
Target: check.MustAbs("/bin/"),
|
Target: MustAbs("/bin/"),
|
||||||
Flags: std.BindOptional,
|
Flags: BindOptional,
|
||||||
}, []stub.Call{
|
}, []stub.Call{
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
||||||
}, nil, []stub.Call{
|
}, nil, []stub.Call{
|
||||||
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
|
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
|
||||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
|
||||||
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil),
|
|
||||||
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
|
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
|
|
||||||
{"ensureFile device", new(Params), &BindMountOp{
|
{"ensureFile device", new(Params), &BindMountOp{
|
||||||
Source: check.MustAbs("/dev/null"),
|
Source: MustAbs("/dev/null"),
|
||||||
Target: check.MustAbs("/dev/null"),
|
Target: MustAbs("/dev/null"),
|
||||||
Flags: std.BindWritable | std.BindDevice,
|
Flags: BindWritable | BindDevice,
|
||||||
}, []stub.Call{
|
}, []stub.Call{
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
|
||||||
}, nil, []stub.Call{
|
}, nil, []stub.Call{
|
||||||
@@ -55,63 +50,60 @@ func TestBindMountOp(t *testing.T) {
|
|||||||
}, stub.UniqueError(5)},
|
}, stub.UniqueError(5)},
|
||||||
|
|
||||||
{"mkdirAll ensure", new(Params), &BindMountOp{
|
{"mkdirAll ensure", new(Params), &BindMountOp{
|
||||||
Source: check.MustAbs("/bin/"),
|
Source: MustAbs("/bin/"),
|
||||||
Target: check.MustAbs("/bin/"),
|
Target: MustAbs("/bin/"),
|
||||||
Flags: std.BindEnsure,
|
Flags: BindEnsure,
|
||||||
}, []stub.Call{
|
}, []stub.Call{
|
||||||
call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, stub.UniqueError(4)),
|
call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, stub.UniqueError(4)),
|
||||||
}, stub.UniqueError(4), nil, nil},
|
}, stub.UniqueError(4), nil, nil},
|
||||||
|
|
||||||
{"success ensure", new(Params), &BindMountOp{
|
{"success ensure", new(Params), &BindMountOp{
|
||||||
Source: check.MustAbs("/bin/"),
|
Source: MustAbs("/bin/"),
|
||||||
Target: check.MustAbs("/usr/bin/"),
|
Target: MustAbs("/usr/bin/"),
|
||||||
Flags: std.BindEnsure,
|
Flags: BindEnsure,
|
||||||
}, []stub.Call{
|
}, []stub.Call{
|
||||||
call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, nil),
|
call("mkdirAll", stub.ExpectArgs{"/bin/", os.FileMode(0700)}, nil, nil),
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
||||||
}, nil, []stub.Call{
|
}, nil, []stub.Call{
|
||||||
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
|
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
|
||||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/usr/bin", os.FileMode(0700)}, nil, nil),
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/usr/bin", os.FileMode(0700)}, nil, nil),
|
||||||
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/usr/bin", uintptr(0x4005)}}, nil, nil),
|
|
||||||
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/usr/bin", uintptr(0x4005), false}, nil, nil),
|
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/usr/bin", uintptr(0x4005), false}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
|
|
||||||
{"success device ro", new(Params), &BindMountOp{
|
{"success device ro", new(Params), &BindMountOp{
|
||||||
Source: check.MustAbs("/dev/null"),
|
Source: MustAbs("/dev/null"),
|
||||||
Target: check.MustAbs("/dev/null"),
|
Target: MustAbs("/dev/null"),
|
||||||
Flags: std.BindDevice,
|
Flags: BindDevice,
|
||||||
}, []stub.Call{
|
}, []stub.Call{
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
|
||||||
}, nil, []stub.Call{
|
}, nil, []stub.Call{
|
||||||
call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil),
|
call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil),
|
||||||
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
|
||||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/dev/null", uintptr(0x4001)}}, nil, nil),
|
|
||||||
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4001), false}, nil, nil),
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4001), false}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
|
|
||||||
{"success device", new(Params), &BindMountOp{
|
{"success device", new(Params), &BindMountOp{
|
||||||
Source: check.MustAbs("/dev/null"),
|
Source: MustAbs("/dev/null"),
|
||||||
Target: check.MustAbs("/dev/null"),
|
Target: MustAbs("/dev/null"),
|
||||||
Flags: std.BindWritable | std.BindDevice,
|
Flags: BindWritable | BindDevice,
|
||||||
}, []stub.Call{
|
}, []stub.Call{
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/dev/null"}, "/dev/null", nil),
|
||||||
}, nil, []stub.Call{
|
}, nil, []stub.Call{
|
||||||
call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil),
|
call("stat", stub.ExpectArgs{"/host/dev/null"}, isDirFi(false), nil),
|
||||||
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
|
call("ensureFile", stub.ExpectArgs{"/sysroot/dev/null", os.FileMode(0444), os.FileMode(0700)}, nil, nil),
|
||||||
call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/dev/null", uintptr(0x4000)}}, nil, nil),
|
|
||||||
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4000), false}, nil, nil),
|
call("bindMount", stub.ExpectArgs{"/host/dev/null", "/sysroot/dev/null", uintptr(0x4000), false}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
|
|
||||||
{"evalSymlinks", new(Params), &BindMountOp{
|
{"evalSymlinks", new(Params), &BindMountOp{
|
||||||
Source: check.MustAbs("/bin/"),
|
Source: MustAbs("/bin/"),
|
||||||
Target: check.MustAbs("/bin/"),
|
Target: MustAbs("/bin/"),
|
||||||
}, []stub.Call{
|
}, []stub.Call{
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", stub.UniqueError(3)),
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", stub.UniqueError(3)),
|
||||||
}, stub.UniqueError(3), nil, nil},
|
}, stub.UniqueError(3), nil, nil},
|
||||||
|
|
||||||
{"stat", new(Params), &BindMountOp{
|
{"stat", new(Params), &BindMountOp{
|
||||||
Source: check.MustAbs("/bin/"),
|
Source: MustAbs("/bin/"),
|
||||||
Target: check.MustAbs("/bin/"),
|
Target: MustAbs("/bin/"),
|
||||||
}, []stub.Call{
|
}, []stub.Call{
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
||||||
}, nil, []stub.Call{
|
}, nil, []stub.Call{
|
||||||
@@ -119,8 +111,8 @@ func TestBindMountOp(t *testing.T) {
|
|||||||
}, stub.UniqueError(2)},
|
}, stub.UniqueError(2)},
|
||||||
|
|
||||||
{"mkdirAll", new(Params), &BindMountOp{
|
{"mkdirAll", new(Params), &BindMountOp{
|
||||||
Source: check.MustAbs("/bin/"),
|
Source: MustAbs("/bin/"),
|
||||||
Target: check.MustAbs("/bin/"),
|
Target: MustAbs("/bin/"),
|
||||||
}, []stub.Call{
|
}, []stub.Call{
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
||||||
}, nil, []stub.Call{
|
}, nil, []stub.Call{
|
||||||
@@ -129,47 +121,30 @@ func TestBindMountOp(t *testing.T) {
|
|||||||
}, stub.UniqueError(1)},
|
}, stub.UniqueError(1)},
|
||||||
|
|
||||||
{"bindMount", new(Params), &BindMountOp{
|
{"bindMount", new(Params), &BindMountOp{
|
||||||
Source: check.MustAbs("/bin/"),
|
Source: MustAbs("/bin/"),
|
||||||
Target: check.MustAbs("/bin/"),
|
Target: MustAbs("/bin/"),
|
||||||
}, []stub.Call{
|
}, []stub.Call{
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
||||||
}, nil, []stub.Call{
|
}, nil, []stub.Call{
|
||||||
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
|
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
|
||||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
|
||||||
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil),
|
|
||||||
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, stub.UniqueError(0)),
|
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, stub.UniqueError(0)),
|
||||||
}, stub.UniqueError(0)},
|
}, stub.UniqueError(0)},
|
||||||
|
|
||||||
{"success eval equals", new(Params), &BindMountOp{
|
|
||||||
Source: check.MustAbs("/bin/"),
|
|
||||||
Target: check.MustAbs("/bin/"),
|
|
||||||
}, []stub.Call{
|
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/bin", nil),
|
|
||||||
}, nil, []stub.Call{
|
|
||||||
call("stat", stub.ExpectArgs{"/host/bin"}, isDirFi(true), nil),
|
|
||||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
|
|
||||||
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil),
|
|
||||||
call("bindMount", stub.ExpectArgs{"/host/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
|
|
||||||
}, nil},
|
|
||||||
|
|
||||||
{"success", new(Params), &BindMountOp{
|
{"success", new(Params), &BindMountOp{
|
||||||
Source: check.MustAbs("/bin/"),
|
Source: MustAbs("/bin/"),
|
||||||
Target: check.MustAbs("/bin/"),
|
Target: MustAbs("/bin/"),
|
||||||
}, []stub.Call{
|
}, []stub.Call{
|
||||||
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
call("evalSymlinks", stub.ExpectArgs{"/bin/"}, "/usr/bin", nil),
|
||||||
}, nil, []stub.Call{
|
}, nil, []stub.Call{
|
||||||
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
|
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil),
|
||||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
|
call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil),
|
||||||
call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil),
|
|
||||||
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
|
call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
|
||||||
}, nil},
|
}, nil},
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unreachable", func(t *testing.T) {
|
t.Run("unreachable", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("nil sourceFinal not optional", func(t *testing.T) {
|
t.Run("nil sourceFinal not optional", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
wantErr := OpStateError("bind")
|
wantErr := OpStateError("bind")
|
||||||
if err := new(BindMountOp).apply(nil, nil); !errors.Is(err, wantErr) {
|
if err := new(BindMountOp).apply(nil, nil); !errors.Is(err, wantErr) {
|
||||||
t.Errorf("apply: error = %v, want %v", err, wantErr)
|
t.Errorf("apply: error = %v, want %v", err, wantErr)
|
||||||
@@ -180,21 +155,21 @@ func TestBindMountOp(t *testing.T) {
|
|||||||
checkOpsValid(t, []opValidTestCase{
|
checkOpsValid(t, []opValidTestCase{
|
||||||
{"nil", (*BindMountOp)(nil), false},
|
{"nil", (*BindMountOp)(nil), false},
|
||||||
{"zero", new(BindMountOp), false},
|
{"zero", new(BindMountOp), false},
|
||||||
{"nil source", &BindMountOp{Target: check.MustAbs("/")}, false},
|
{"nil source", &BindMountOp{Target: MustAbs("/")}, false},
|
||||||
{"nil target", &BindMountOp{Source: check.MustAbs("/")}, false},
|
{"nil target", &BindMountOp{Source: MustAbs("/")}, false},
|
||||||
{"flag optional ensure", &BindMountOp{Source: check.MustAbs("/"), Target: check.MustAbs("/"), Flags: std.BindOptional | std.BindEnsure}, false},
|
{"flag optional ensure", &BindMountOp{Source: MustAbs("/"), Target: MustAbs("/"), Flags: BindOptional | BindEnsure}, false},
|
||||||
{"valid", &BindMountOp{Source: check.MustAbs("/"), Target: check.MustAbs("/")}, true},
|
{"valid", &BindMountOp{Source: MustAbs("/"), Target: MustAbs("/")}, true},
|
||||||
})
|
})
|
||||||
|
|
||||||
checkOpsBuilder(t, []opsBuilderTestCase{
|
checkOpsBuilder(t, []opsBuilderTestCase{
|
||||||
{"autoetc", new(Ops).Bind(
|
{"autoetc", new(Ops).Bind(
|
||||||
check.MustAbs("/etc/"),
|
MustAbs("/etc/"),
|
||||||
check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||||
0,
|
0,
|
||||||
), Ops{
|
), Ops{
|
||||||
&BindMountOp{
|
&BindMountOp{
|
||||||
Source: check.MustAbs("/etc/"),
|
Source: MustAbs("/etc/"),
|
||||||
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
})
|
})
|
||||||
@@ -203,45 +178,45 @@ func TestBindMountOp(t *testing.T) {
|
|||||||
{"zero", new(BindMountOp), new(BindMountOp), false},
|
{"zero", new(BindMountOp), new(BindMountOp), false},
|
||||||
|
|
||||||
{"internal ne", &BindMountOp{
|
{"internal ne", &BindMountOp{
|
||||||
Source: check.MustAbs("/etc/"),
|
Source: MustAbs("/etc/"),
|
||||||
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||||
}, &BindMountOp{
|
}, &BindMountOp{
|
||||||
Source: check.MustAbs("/etc/"),
|
Source: MustAbs("/etc/"),
|
||||||
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||||
sourceFinal: check.MustAbs("/etc/"),
|
sourceFinal: MustAbs("/etc/"),
|
||||||
}, true},
|
}, true},
|
||||||
|
|
||||||
{"flags differs", &BindMountOp{
|
{"flags differs", &BindMountOp{
|
||||||
Source: check.MustAbs("/etc/"),
|
Source: MustAbs("/etc/"),
|
||||||
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||||
}, &BindMountOp{
|
}, &BindMountOp{
|
||||||
Source: check.MustAbs("/etc/"),
|
Source: MustAbs("/etc/"),
|
||||||
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||||
Flags: std.BindOptional,
|
Flags: BindOptional,
|
||||||
}, false},
|
}, false},
|
||||||
|
|
||||||
{"source differs", &BindMountOp{
|
{"source differs", &BindMountOp{
|
||||||
Source: check.MustAbs("/.hakurei/etc/"),
|
Source: MustAbs("/.hakurei/etc/"),
|
||||||
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||||
}, &BindMountOp{
|
}, &BindMountOp{
|
||||||
Source: check.MustAbs("/etc/"),
|
Source: MustAbs("/etc/"),
|
||||||
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||||
}, false},
|
}, false},
|
||||||
|
|
||||||
{"target differs", &BindMountOp{
|
{"target differs", &BindMountOp{
|
||||||
Source: check.MustAbs("/etc/"),
|
Source: MustAbs("/etc/"),
|
||||||
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||||
}, &BindMountOp{
|
}, &BindMountOp{
|
||||||
Source: check.MustAbs("/etc/"),
|
Source: MustAbs("/etc/"),
|
||||||
Target: check.MustAbs("/etc/"),
|
Target: MustAbs("/etc/"),
|
||||||
}, false},
|
}, false},
|
||||||
|
|
||||||
{"equals", &BindMountOp{
|
{"equals", &BindMountOp{
|
||||||
Source: check.MustAbs("/etc/"),
|
Source: MustAbs("/etc/"),
|
||||||
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||||
}, &BindMountOp{
|
}, &BindMountOp{
|
||||||
Source: check.MustAbs("/etc/"),
|
Source: MustAbs("/etc/"),
|
||||||
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||||
}, true},
|
}, true},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -249,14 +224,14 @@ func TestBindMountOp(t *testing.T) {
|
|||||||
{"invalid", new(BindMountOp), "mounting", "<invalid>"},
|
{"invalid", new(BindMountOp), "mounting", "<invalid>"},
|
||||||
|
|
||||||
{"autoetc", &BindMountOp{
|
{"autoetc", &BindMountOp{
|
||||||
Source: check.MustAbs("/etc/"),
|
Source: MustAbs("/etc/"),
|
||||||
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||||
}, "mounting", `"/etc/" on "/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659" flags 0x0`},
|
}, "mounting", `"/etc/" on "/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659" flags 0x0`},
|
||||||
|
|
||||||
{"hostdev", &BindMountOp{
|
{"hostdev", &BindMountOp{
|
||||||
Source: check.MustAbs("/dev/"),
|
Source: MustAbs("/dev/"),
|
||||||
Target: check.MustAbs("/dev/"),
|
Target: MustAbs("/dev/"),
|
||||||
Flags: std.BindWritable | std.BindDevice,
|
Flags: BindWritable | BindDevice,
|
||||||
}, "mounting", `"/dev/" flags 0x6`},
|
}, "mounting", `"/dev/" flags 0x6`},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/gob"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
"hakurei.app/fhs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() { gob.Register(new(DaemonOp)) }
|
|
||||||
|
|
||||||
const (
|
|
||||||
// daemonTimeout is the duration a [DaemonOp] is allowed to block before the
|
|
||||||
// [DaemonOp.Target] marker becomes available.
|
|
||||||
daemonTimeout = 5 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
// Daemon is a helper for appending [DaemonOp] to [Ops].
|
|
||||||
func (f *Ops) Daemon(target, path *check.Absolute, args ...string) *Ops {
|
|
||||||
*f = append(*f, &DaemonOp{target, path, args})
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
|
||||||
// DaemonOp starts a daemon in the container and blocks until Target appears.
|
|
||||||
type DaemonOp struct {
|
|
||||||
// Pathname indicating readiness of daemon.
|
|
||||||
Target *check.Absolute
|
|
||||||
// Absolute pathname passed to [exec.Cmd].
|
|
||||||
Path *check.Absolute
|
|
||||||
// Arguments (excl. first) passed to [exec.Cmd].
|
|
||||||
Args []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// earlyTerminationError is returned by [DaemonOp] when a daemon terminates
|
|
||||||
// before [DaemonOp.Target] appears.
|
|
||||||
type earlyTerminationError struct {
|
|
||||||
// Returned by [DaemonOp.String].
|
|
||||||
op string
|
|
||||||
// Copied from wait4 loop.
|
|
||||||
wstatus syscall.WaitStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *earlyTerminationError) Error() string {
|
|
||||||
res := ""
|
|
||||||
switch {
|
|
||||||
case e.wstatus.Exited():
|
|
||||||
res = "exit status " + strconv.Itoa(e.wstatus.ExitStatus())
|
|
||||||
case e.wstatus.Signaled():
|
|
||||||
res = "signal: " + e.wstatus.Signal().String()
|
|
||||||
case e.wstatus.Stopped():
|
|
||||||
res = "stop signal: " + e.wstatus.StopSignal().String()
|
|
||||||
if e.wstatus.StopSignal() == syscall.SIGTRAP && e.wstatus.TrapCause() != 0 {
|
|
||||||
res += " (trap " + strconv.Itoa(e.wstatus.TrapCause()) + ")"
|
|
||||||
}
|
|
||||||
case e.wstatus.Continued():
|
|
||||||
res = "continued"
|
|
||||||
}
|
|
||||||
if e.wstatus.CoreDump() {
|
|
||||||
res += " (core dumped)"
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *earlyTerminationError) Message() string { return e.op + " " + e.Error() }
|
|
||||||
|
|
||||||
func (d *DaemonOp) Valid() bool { return d != nil && d.Target != nil && d.Path != nil }
|
|
||||||
func (d *DaemonOp) early(*setupState, syscallDispatcher) error { return nil }
|
|
||||||
func (d *DaemonOp) apply(*setupState, syscallDispatcher) error { return nil }
|
|
||||||
func (d *DaemonOp) late(state *setupState, k syscallDispatcher) error {
|
|
||||||
cmd := exec.CommandContext(state.Context, d.Path.String(), d.Args...)
|
|
||||||
cmd.Env = state.Env
|
|
||||||
cmd.Dir = fhs.Root
|
|
||||||
if state.IsVerbose() {
|
|
||||||
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
|
||||||
}
|
|
||||||
// WaitDelay: left unset because lifetime is bound by AdoptWaitDelay on cancellation
|
|
||||||
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGTERM) }
|
|
||||||
|
|
||||||
state.Verbosef("starting %s", d.String())
|
|
||||||
if err := k.start(cmd); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
deadline := time.Now().Add(daemonTimeout)
|
|
||||||
var wstatusErr error
|
|
||||||
|
|
||||||
for {
|
|
||||||
if _, err := k.stat(d.Target.String()); err != nil {
|
|
||||||
if !errors.Is(err, os.ErrNotExist) {
|
|
||||||
_ = k.signal(cmd, os.Kill)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if time.Now().After(deadline) {
|
|
||||||
_ = k.signal(cmd, os.Kill)
|
|
||||||
return context.DeadlineExceeded
|
|
||||||
}
|
|
||||||
|
|
||||||
if wstatusErr != nil {
|
|
||||||
return wstatusErr
|
|
||||||
}
|
|
||||||
if wstatus, ok := state.terminated(cmd.Process.Pid); ok {
|
|
||||||
// check once again: process could have satisfied Target between stat and the lookup
|
|
||||||
wstatusErr = &earlyTerminationError{d.String(), wstatus}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(500 * time.Microsecond)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
state.Verbosef("daemon process %d ready", cmd.Process.Pid)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DaemonOp) Is(op Op) bool {
|
|
||||||
vd, ok := op.(*DaemonOp)
|
|
||||||
return ok && d.Valid() && vd.Valid() &&
|
|
||||||
d.Target.Is(vd.Target) && d.Path.Is(vd.Path) &&
|
|
||||||
slices.Equal(d.Args, vd.Args)
|
|
||||||
}
|
|
||||||
func (*DaemonOp) prefix() (string, bool) { return zeroString, false }
|
|
||||||
func (d *DaemonOp) String() string { return fmt.Sprintf("daemon providing %q", d.Target) }
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
"hakurei.app/internal/stub"
|
|
||||||
"hakurei.app/message"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEarlyTerminationError(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
err error
|
|
||||||
want string
|
|
||||||
msg string
|
|
||||||
}{
|
|
||||||
{"exited", &earlyTerminationError{
|
|
||||||
`daemon providing "/run/user/1971/pulse/native"`, 127 << 8,
|
|
||||||
}, "exit status 127", `daemon providing "/run/user/1971/pulse/native" exit status 127`},
|
|
||||||
}
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
if got := tc.err.Error(); got != tc.want {
|
|
||||||
t.Errorf("Error: %q, want %q", got, tc.want)
|
|
||||||
}
|
|
||||||
if got := tc.err.(message.Error).Message(); got != tc.msg {
|
|
||||||
t.Errorf("Message: %s, want %s", got, tc.msg)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDaemonOp(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
checkSimple(t, "DaemonOp.late", []simpleTestCase{
|
|
||||||
{"success", func(k *kstub) error {
|
|
||||||
state := setupState{Params: &Params{Env: []string{"\x00"}}, Context: t.Context(), Msg: k}
|
|
||||||
return (&DaemonOp{
|
|
||||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
|
||||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
|
||||||
Args: []string{"-v"},
|
|
||||||
}).late(&state, k)
|
|
||||||
}, stub.Expect{Calls: []stub.Call{
|
|
||||||
call("isVerbose", stub.ExpectArgs{}, true, nil),
|
|
||||||
call("verbosef", stub.ExpectArgs{"starting %s", []any{`daemon providing "/run/user/1971/pulse/native"`}}, nil, nil),
|
|
||||||
call("start", stub.ExpectArgs{"/run/current-system/sw/bin/pipewire-pulse", []string{"/run/current-system/sw/bin/pipewire-pulse", "-v"}, []string{"\x00"}, "/"}, &os.Process{Pid: 0xcafe}, nil),
|
|
||||||
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
|
|
||||||
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
|
|
||||||
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), os.ErrNotExist),
|
|
||||||
call("stat", stub.ExpectArgs{"/run/user/1971/pulse/native"}, isDirFi(false), nil),
|
|
||||||
call("verbosef", stub.ExpectArgs{"daemon process %d ready", []any{0xcafe}}, nil, nil),
|
|
||||||
}}, nil},
|
|
||||||
})
|
|
||||||
|
|
||||||
checkOpsValid(t, []opValidTestCase{
|
|
||||||
{"nil", (*DaemonOp)(nil), false},
|
|
||||||
{"zero", new(DaemonOp), false},
|
|
||||||
{"valid", &DaemonOp{
|
|
||||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
|
||||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
|
||||||
Args: []string{"-v"},
|
|
||||||
}, true},
|
|
||||||
})
|
|
||||||
|
|
||||||
checkOpsBuilder(t, []opsBuilderTestCase{
|
|
||||||
{"pipewire-pulse", new(Ops).Daemon(
|
|
||||||
check.MustAbs("/run/user/1971/pulse/native"),
|
|
||||||
check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"), "-v",
|
|
||||||
), Ops{
|
|
||||||
&DaemonOp{
|
|
||||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
|
||||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
|
||||||
Args: []string{"-v"},
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
})
|
|
||||||
|
|
||||||
checkOpIs(t, []opIsTestCase{
|
|
||||||
{"zero", new(DaemonOp), new(DaemonOp), false},
|
|
||||||
|
|
||||||
{"args differs", &DaemonOp{
|
|
||||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
|
||||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
|
||||||
Args: []string{"-v"},
|
|
||||||
}, &DaemonOp{
|
|
||||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
|
||||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
|
||||||
}, false},
|
|
||||||
|
|
||||||
{"path differs", &DaemonOp{
|
|
||||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
|
||||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire"),
|
|
||||||
}, &DaemonOp{
|
|
||||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
|
||||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
|
||||||
}, false},
|
|
||||||
|
|
||||||
{"target differs", &DaemonOp{
|
|
||||||
Target: check.MustAbs("/run/user/65534/pulse/native"),
|
|
||||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
|
||||||
}, &DaemonOp{
|
|
||||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
|
||||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
|
||||||
}, false},
|
|
||||||
|
|
||||||
{"equals", &DaemonOp{
|
|
||||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
|
||||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
|
||||||
}, &DaemonOp{
|
|
||||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
|
||||||
Path: check.MustAbs("/run/current-system/sw/bin/pipewire-pulse"),
|
|
||||||
}, true},
|
|
||||||
})
|
|
||||||
|
|
||||||
checkOpMeta(t, []opMetaTestCase{
|
|
||||||
{"pipewire-pulse", &DaemonOp{
|
|
||||||
Target: check.MustAbs("/run/user/1971/pulse/native"),
|
|
||||||
}, zeroString, `daemon providing "/run/user/1971/pulse/native"`},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
+20
-26
@@ -3,26 +3,21 @@ package container
|
|||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path"
|
||||||
. "syscall"
|
. "syscall"
|
||||||
|
|
||||||
"hakurei.app/check"
|
|
||||||
"hakurei.app/fhs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() { gob.Register(new(MountDevOp)) }
|
func init() { gob.Register(new(MountDevOp)) }
|
||||||
|
|
||||||
// Dev appends an [Op] that mounts a subset of host /dev.
|
// Dev appends an [Op] that mounts a subset of host /dev.
|
||||||
func (f *Ops) Dev(target *check.Absolute, mqueue bool) *Ops {
|
func (f *Ops) Dev(target *Absolute, mqueue bool) *Ops {
|
||||||
*f = append(*f, &MountDevOp{target, mqueue, false})
|
*f = append(*f, &MountDevOp{target, mqueue, false})
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
// DevWritable appends an [Op] that mounts a writable subset of host /dev.
|
// DevWritable appends an [Op] that mounts a writable subset of host /dev.
|
||||||
//
|
// There is usually no good reason to write to /dev, so this should always be followed by a [RemountOp].
|
||||||
// There is usually no good reason to write to /dev, so this should always be
|
func (f *Ops) DevWritable(target *Absolute, mqueue bool) *Ops {
|
||||||
// followed by a [RemountOp].
|
|
||||||
func (f *Ops) DevWritable(target *check.Absolute, mqueue bool) *Ops {
|
|
||||||
*f = append(*f, &MountDevOp{target, mqueue, true})
|
*f = append(*f, &MountDevOp{target, mqueue, true})
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
@@ -31,7 +26,7 @@ func (f *Ops) DevWritable(target *check.Absolute, mqueue bool) *Ops {
|
|||||||
// If Mqueue is true, a private instance of [FstypeMqueue] is mounted.
|
// If Mqueue is true, a private instance of [FstypeMqueue] is mounted.
|
||||||
// If Write is true, the resulting mount point is left writable.
|
// If Write is true, the resulting mount point is left writable.
|
||||||
type MountDevOp struct {
|
type MountDevOp struct {
|
||||||
Target *check.Absolute
|
Target *Absolute
|
||||||
Mqueue bool
|
Mqueue bool
|
||||||
Write bool
|
Write bool
|
||||||
}
|
}
|
||||||
@@ -46,39 +41,39 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
|
for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
|
||||||
targetPath := filepath.Join(target, name)
|
targetPath := path.Join(target, name)
|
||||||
if err := k.ensureFile(targetPath, 0444, state.ParentPerm); err != nil {
|
if err := k.ensureFile(targetPath, 0444, state.ParentPerm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := k.bindMount(
|
if err := k.bindMount(
|
||||||
state,
|
toHost(FHSDev+name),
|
||||||
toHost(fhs.Dev+name),
|
|
||||||
targetPath,
|
targetPath,
|
||||||
0,
|
0,
|
||||||
|
true,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i, name := range []string{"stdin", "stdout", "stderr"} {
|
for i, name := range []string{"stdin", "stdout", "stderr"} {
|
||||||
if err := k.symlink(
|
if err := k.symlink(
|
||||||
fhs.Proc+"self/fd/"+string(rune(i+'0')),
|
FHSProc+"self/fd/"+string(rune(i+'0')),
|
||||||
filepath.Join(target, name),
|
path.Join(target, name),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, pair := range [][2]string{
|
for _, pair := range [][2]string{
|
||||||
{fhs.Proc + "self/fd", "fd"},
|
{FHSProc + "self/fd", "fd"},
|
||||||
{fhs.Proc + "kcore", "core"},
|
{FHSProc + "kcore", "core"},
|
||||||
{"pts/ptmx", "ptmx"},
|
{"pts/ptmx", "ptmx"},
|
||||||
} {
|
} {
|
||||||
if err := k.symlink(pair[0], filepath.Join(target, pair[1])); err != nil {
|
if err := k.symlink(pair[0], path.Join(target, pair[1])); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
devShmPath := filepath.Join(target, "shm")
|
devShmPath := path.Join(target, "shm")
|
||||||
devPtsPath := filepath.Join(target, "pts")
|
devPtsPath := path.Join(target, "pts")
|
||||||
for _, name := range []string{devShmPath, devPtsPath} {
|
for _, name := range []string{devShmPath, devPtsPath} {
|
||||||
if err := k.mkdir(name, state.ParentPerm); err != nil {
|
if err := k.mkdir(name, state.ParentPerm); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -92,17 +87,17 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
|
|
||||||
if state.RetainSession {
|
if state.RetainSession {
|
||||||
if k.isatty(Stdout) {
|
if k.isatty(Stdout) {
|
||||||
consolePath := filepath.Join(target, "console")
|
consolePath := path.Join(target, "console")
|
||||||
if err := k.ensureFile(consolePath, 0444, state.ParentPerm); err != nil {
|
if err := k.ensureFile(consolePath, 0444, state.ParentPerm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if name, err := k.readlink(hostProc.stdout()); err != nil {
|
if name, err := k.readlink(hostProc.stdout()); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if err = k.bindMount(
|
} else if err = k.bindMount(
|
||||||
state,
|
|
||||||
toHost(name),
|
toHost(name),
|
||||||
consolePath,
|
consolePath,
|
||||||
0,
|
0,
|
||||||
|
false,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -110,7 +105,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if d.Mqueue {
|
if d.Mqueue {
|
||||||
mqueueTarget := filepath.Join(target, "mqueue")
|
mqueueTarget := path.Join(target, "mqueue")
|
||||||
if err := k.mkdir(mqueueTarget, state.ParentPerm); err != nil {
|
if err := k.mkdir(mqueueTarget, state.ParentPerm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -123,12 +118,11 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := k.remount(state, target, MS_RDONLY); err != nil {
|
if err := k.remount(target, MS_RDONLY); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777)
|
return k.mountTmpfs(SourceTmpfs, devShmPath, MS_NOSUID|MS_NODEV, 0, 01777)
|
||||||
}
|
}
|
||||||
func (d *MountDevOp) late(*setupState, syscallDispatcher) error { return nil }
|
|
||||||
|
|
||||||
func (d *MountDevOp) Is(op Op) bool {
|
func (d *MountDevOp) Is(op Op) bool {
|
||||||
vd, ok := op.(*MountDevOp)
|
vd, ok := op.(*MountDevOp)
|
||||||
@@ -137,7 +131,7 @@ func (d *MountDevOp) Is(op Op) bool {
|
|||||||
d.Mqueue == vd.Mqueue &&
|
d.Mqueue == vd.Mqueue &&
|
||||||
d.Write == vd.Write
|
d.Write == vd.Write
|
||||||
}
|
}
|
||||||
func (*MountDevOp) prefix() (string, bool) { return "mounting", true }
|
func (*MountDevOp) prefix() string { return "mounting" }
|
||||||
func (d *MountDevOp) String() string {
|
func (d *MountDevOp) String() string {
|
||||||
if d.Mqueue {
|
if d.Mqueue {
|
||||||
return fmt.Sprintf("dev on %q with mqueue", d.Target)
|
return fmt.Sprintf("dev on %q with mqueue", d.Target)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user