Compare commits

...

9 Commits

Author SHA1 Message Date
71135f339a
release: 0.2.18
All checks were successful
Test / Create distribution (push) Successful in 20s
Release / Create release (push) Successful in 33s
Test / Fortify (push) Successful in 2m4s
Test / Data race detector (push) Successful in 2m33s
Test / Flake checks (push) Successful in 48s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 18:52:33 +09:00
b6af8caffe
nix: clean up directory structure
All checks were successful
Test / Create distribution (push) Successful in 19s
Test / Fortify (push) Successful in 36s
Test / Data race detector (push) Successful in 56s
Test / Flake checks (push) Successful in 41s
Tests for fpkg is going to be in ./cmd/fpkg, so this central tests directory is no longer necessary.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 18:48:01 +09:00
e1a3549ea0
workflows: separate nixos tests from flake check
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Fortify (push) Successful in 2m12s
Test / Data race detector (push) Successful in 3m0s
Test / Flake checks (push) Successful in 41s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 18:34:42 +09:00
8bf162820b
nix: separate fsu from package
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Run NixOS test (push) Successful in 7m25s
This appears to be the only way to build them with different configuration. This enables static linking in the main package.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 18:13:37 +09:00
dccb366608
ldd: handle behaviour on static executable
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m27s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 18:02:33 +09:00
83c8f0488b
ldd: pass absolute path to bwrap
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Run NixOS test (push) Successful in 3m31s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 17:46:22 +09:00
478b27922c
fortify: handle errors via MustParse
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m29s
The errSuccess behaviour is kept for beforeExit.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 12:57:59 +09:00
ba1498cd18
command: filter parse errors
All checks were successful
Test / Create distribution (push) Successful in 25s
Test / Run NixOS test (push) Successful in 3m29s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 12:55:10 +09:00
eda4d612c2
fortify: keep external files alive
All checks were successful
Test / Create distribution (push) Successful in 19s
Test / Run NixOS test (push) Successful in 3m10s
This should eliminate sporadic failures, like the known double close in "seccomp".

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-02-23 03:24:37 +09:00
21 changed files with 241 additions and 110 deletions

View File

@ -5,26 +5,53 @@ on:
- pull_request
jobs:
test:
name: Run NixOS test
fortify:
name: Fortify
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run fortify tests
run: nix build --out-link "result-fortify" --print-out-paths --print-build-logs .#checks.x86_64-linux.fortify
- name: Run flake checks
run: nix --print-build-logs --experimental-features 'nix-command flakes' flake check
- name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.fortify
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "fortify-vm-output"
path: result-fortify/*
path: result/*
retention-days: 1
race:
name: Data race detector
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run NixOS test
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.race
- name: Upload test output
uses: actions/upload-artifact@v3
with:
name: "fortify-race-vm-output"
path: result/*
retention-days: 1
check:
name: Flake checks
needs:
- fortify
- race
runs-on: nix
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run checks
run: nix --print-build-logs --experimental-features 'nix-command flakes' flake check
dist:
name: Create distribution
runs-on: nix

19
cmd/fsu/package.nix Normal file
View File

@ -0,0 +1,19 @@
{
buildGoModule,
fortify ? abort "fortify package required",
}:
buildGoModule {
pname = "${fortify.pname}-fsu";
inherit (fortify) version;
src = ./.;
inherit (fortify) vendorHash;
CGO_ENABLED = 0;
preBuild = ''
go mod init fsu >& /dev/null
'';
ldflags = [ "-X main.Fmain=${fortify}/libexec/fortify" ];
}

View File

@ -29,6 +29,11 @@ type (
Command interface {
Parse(arguments []string) error
// MustParse determines exit outcomes for Parse errors
// and calls handleError if [HandlerFunc] returns a non-nil error.
MustParse(arguments []string, handleError func(error))
baseNode[Command]
}
Node baseNode[Node]

View File

@ -3,6 +3,7 @@ package command
import (
"errors"
"log"
"os"
)
var (
@ -78,3 +79,27 @@ func (n *node) printf(format string, a ...any) {
n.logf(format, a...)
}
}
func (n *node) MustParse(arguments []string, handleError func(error)) {
switch err := n.Parse(arguments); err {
case nil:
return
case ErrHelp:
os.Exit(0)
case ErrNoMatch:
os.Exit(1)
case ErrEmptyTree:
os.Exit(1)
default:
var flagError FlagError
if !errors.As(err, &flagError) { // returned by HandlerFunc
handleError(err)
os.Exit(1)
}
if flagError.Success() {
os.Exit(0)
}
os.Exit(1)
}
}

View File

@ -57,6 +57,12 @@
;
in
{
fortify = callPackage ./test { inherit system self; };
race = callPackage ./test {
inherit system self;
withRace = true;
};
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
cd ${./.}
@ -85,8 +91,6 @@
touch $out
'';
fortify = callPackage ./tests/fortify { inherit system self; };
}
);
@ -98,7 +102,10 @@
in
{
default = self.packages.${system}.fortify;
fortify = pkgs.callPackage ./package.nix { };
fortify = pkgs.pkgsStatic.callPackage ./package.nix {
inherit (pkgs) bubblewrap xdg-dbus-proxy glibc;
};
fsu = pkgs.callPackage ./cmd/fsu/package.nix { inherit (self.packages.${system}) fortify; };
dist =
pkgs.runCommand "${fortify.name}-dist" { inherit (self.devShells.${system}.default) buildInputs; }

View File

@ -5,6 +5,7 @@ import (
"errors"
"io"
"os"
"runtime"
)
// NewWriterTo returns a [File] that receives content from wt on fulfillment.
@ -25,13 +26,20 @@ func (f *writeToFile) Fulfill(ctx context.Context, dispatchErr func(error)) erro
f.Set(r)
done := make(chan struct{})
go func() { _, err = f.wt.WriteTo(w); dispatchErr(err); dispatchErr(w.Close()); close(done) }()
go func() {
_, err = f.wt.WriteTo(w)
dispatchErr(err)
dispatchErr(w.Close())
close(done)
runtime.KeepAlive(r)
}()
go func() {
select {
case <-done:
dispatchErr(nil)
case <-ctx.Done():
dispatchErr(w.Close()) // this aborts WriteTo with file already closed
runtime.KeepAlive(r)
}
}()
@ -83,6 +91,7 @@ func (f *statFile) Fulfill(ctx context.Context, dispatchErr func(error)) error {
default:
panic("unreachable")
}
runtime.KeepAlive(w)
}()
go func() {
@ -91,6 +100,7 @@ func (f *statFile) Fulfill(ctx context.Context, dispatchErr func(error)) error {
dispatchErr(nil)
case <-ctx.Done():
dispatchErr(r.Close()) // this aborts Read with file already closed
runtime.KeepAlive(w)
}
}()

View File

@ -27,7 +27,12 @@ func (e *exporter) prepare() error {
}
ec := make(chan error, 1)
go func(fd uintptr) { ec <- exportFilter(fd, e.opts); close(ec); _ = e.closeWrite() }(e.w.Fd())
go func(fd uintptr) {
ec <- exportFilter(fd, e.opts)
close(ec)
_ = e.closeWrite()
runtime.KeepAlive(e.w)
}(e.w.Fd())
e.exportErr = ec
runtime.SetFinalizer(e, (*exporter).closeWrite)
})

View File

@ -1,9 +1,10 @@
package ldd
import (
"bytes"
"context"
"os"
"strings"
"os/exec"
"time"
"git.gensokyo.uk/security/fortify/helper"
@ -12,27 +13,31 @@ import (
const lddTimeout = 2 * time.Second
var (
msgStaticGlibc = []byte("not a dynamic executable")
)
func Exec(ctx context.Context, p string) ([]*Entry, error) {
var h helper.Helper
if b, err := helper.NewBwrap(
if toolPath, err := exec.LookPath("ldd"); err != nil {
return nil, err
} else if h, err = helper.NewBwrap(
(&bwrap.Config{
Hostname: "fortify-ldd",
Chdir: "/",
Syscall: &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
NewSession: true,
DieWithParent: true,
}).Bind("/", "/").DevTmpfs("/dev"), "ldd",
}).Bind("/", "/").DevTmpfs("/dev"), toolPath,
nil, func(_, _ int) []string { return []string{p} },
nil, nil,
); err != nil {
return nil, err
} else {
h = b
}
stdout := new(strings.Builder)
h.Stdout(stdout).Stderr(os.Stderr)
stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
h.Stdout(stdout).Stderr(stderr)
c, cancel := context.WithTimeout(ctx, lddTimeout)
defer cancel()
@ -40,6 +45,12 @@ func Exec(ctx context.Context, p string) ([]*Entry, error) {
return nil, err
}
if err := h.Wait(); err != nil {
m := stderr.Bytes()
if bytes.Contains(m, msgStaticGlibc) {
return nil, nil
}
_, _ = os.Stderr.Write(m)
return nil, err
}

28
main.go
View File

@ -53,32 +53,16 @@ func main() {
log.Fatal("this program must not run as root")
}
err := buildCommand(os.Stderr).Parse(os.Args[1:])
if errors.Is(err, errSuccess) || errors.Is(err, command.ErrHelp) {
internal.Exit(0)
panic("unreachable")
buildCommand(os.Stderr).MustParse(os.Args[1:], func(err error) {
fmsg.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) {
fmsg.BeforeExit()
os.Exit(0)
}
if errors.Is(err, command.ErrNoMatch) || errors.Is(err, command.ErrEmptyTree) {
internal.Exit(1)
panic("unreachable")
}
if err == nil {
})
log.Fatal("unreachable")
}
var flagError command.FlagError
if !errors.As(err, &flagError) {
log.Printf("command: %v", err)
internal.Exit(1)
panic("unreachable")
}
fmsg.Verbose(flagError.Error())
if flagError.Success() {
internal.Exit(0)
}
internal.Exit(1)
}
func buildCommand(out io.Writer) command.Command {
var (
flagVerbose bool

View File

@ -30,7 +30,7 @@ in
config = mkIf cfg.enable {
security.wrappers.fsu = {
source = "${cfg.package}/libexec/fsu";
source = "${cfg.fsuPackage}/bin/fsu";
setuid = true;
owner = "root";
setgid = true;

View File

@ -36,7 +36,7 @@ package
*Default:*
` <derivation fortify-0.2.17> `
` <derivation fortify-static-x86_64-unknown-linux-musl-0.2.18> `
@ -670,6 +670,25 @@ boolean
## environment\.fortify\.fsuPackage
The fsu package to use\.
*Type:*
package
*Default:*
` <derivation fortify-fsu-0.2.18> `
## environment\.fortify\.home-manager

View File

@ -2,6 +2,9 @@
let
inherit (lib) types mkOption mkEnableOption;
fortify = pkgs.pkgsStatic.callPackage ./package.nix {
inherit (pkgs) bubblewrap xdg-dbus-proxy glibc;
};
in
{
@ -11,10 +14,16 @@ in
package = mkOption {
type = types.package;
default = pkgs.callPackage ./package.nix { };
default = fortify;
description = "The fortify package to use.";
};
fsuPackage = mkOption {
type = types.package;
default = pkgs.callPackage ./cmd/fsu/package.nix { inherit fortify; };
description = "The fsu package to use.";
};
users = mkOption {
type =
let

View File

@ -1,5 +1,6 @@
{
lib,
stdenv,
buildGoModule,
makeBinaryWrapper,
xdg-dbus-proxy,
@ -12,16 +13,22 @@
wayland-protocols,
wayland-scanner,
xorg,
glibc, # for ldd
withStatic ? stdenv.hostPlatform.isStatic,
}:
buildGoModule rec {
pname = "fortify";
version = "0.2.17";
version = "0.2.18";
src = builtins.path {
name = "fortify-src";
name = "${pname}-src";
path = lib.cleanSource ./.;
filter = path: type: !(type != "directory" && lib.hasSuffix ".nix" path);
filter =
path: type:
!(type == "regular" && lib.hasSuffix ".nix" path)
&& !(type == "directory" && lib.hasSuffix "/cmd/fsu" path);
};
vendorHash = null;
@ -31,17 +38,22 @@ buildGoModule rec {
ldflags: name: value:
ldflags ++ [ "-X git.gensokyo.uk/security/fortify/internal.${name}=${value}" ]
)
(
[
"-s -w"
"-X main.Fmain=${placeholder "out"}/libexec/fortify"
]
++ lib.optionals withStatic [
"-linkmode external"
"-extldflags \"-static\""
]
)
{
Version = "v${version}";
Fsu = "/run/wrappers/bin/fsu";
};
# nix build environment does not allow acls
GO_TEST_SKIP_ACL = 1;
env.GO_TEST_SKIP_ACL = 1;
buildInputs =
[
@ -64,7 +76,7 @@ buildGoModule rec {
];
preBuild = ''
HOME=$(mktemp -d) go generate ./...
HOME="$(mktemp -d)" PATH="${pkg-config}/bin:$PATH" go generate ./...
'';
postInstall = ''
@ -76,6 +88,7 @@ buildGoModule rec {
makeBinaryWrapper "$out/libexec/fortify" "$out/bin/fortify" \
--inherit-argv0 --prefix PATH : ${
lib.makeBinPath [
glibc
bubblewrap
xdg-dbus-proxy
]

47
test/default.nix Normal file
View File

@ -0,0 +1,47 @@
{
lib,
nixosTest,
writeShellScriptBin,
system,
self,
withRace ? false,
}:
nixosTest {
name = "fortify" + (if withRace then "-race" else "");
nodes.machine =
{ options, pkgs, ... }:
{
environment.systemPackages = [
# For go tests:
self.packages.${system}.fhs
(writeShellScriptBin "fortify-src" "echo -n ${self.packages.${system}.fortify.src}")
];
# Run with Go race detector:
environment.fortify = lib.mkIf withRace rec {
# race detector does not support static linking
package = (pkgs.callPackage ../package.nix { }).overrideAttrs (previousAttrs: {
GOFLAGS = previousAttrs.GOFLAGS ++ [ "-race" ];
});
fsuPackage = options.environment.fortify.fsuPackage.default.override { fortify = package; };
};
imports = [
./configuration.nix
self.nixosModules.fortify
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;
}

View File

@ -1,51 +0,0 @@
{
system,
self,
nixosTest,
writeShellScriptBin,
}:
nixosTest {
name = "fortify";
nodes.machine = {
environment.systemPackages = [
# For go tests:
self.packages.${system}.fhs
(writeShellScriptBin "fortify-src" "echo -n ${self.packages.${system}.fortify.src}")
];
# Run with Go race detector:
environment.fortify.package =
let
inherit (self.packages.${system}) fortify;
in
fortify.overrideAttrs (previousAttrs: {
GOFLAGS = previousAttrs.GOFLAGS ++ [ "-race" ];
# fsu does not like cgo
disallowedReferences = previousAttrs.disallowedReferences ++ [ fortify ];
postInstall =
previousAttrs.postInstall
+ ''
cp -a "${fortify}/libexec/fsu" "$out/libexec/fsu"
sed -i 's:${fortify}:${placeholder "out"}:' "$out/libexec/fsu"
'';
});
imports = [
./configuration.nix
self.nixosModules.fortify
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;
}

View File

@ -94,6 +94,7 @@ func bindRawConn(done chan struct{}, rc syscall.RawConn, p, appID, instanceID st
// keep socket alive until done is requested
<-done
runtime.KeepAlive(syncPipe[1].Fd())
}); err != nil {
setupDone <- err
}

View File

@ -23,7 +23,7 @@ static const struct wl_registry_listener registry_listener = {
.global_remove = registry_handle_global_remove,
};
int32_t bind_wayland_fd(char *socket_path, int fd, const char *app_id, const char *instance_id, int sync_fd) {
int32_t f_bind_wayland_fd(char *socket_path, int fd, const char *app_id, const char *instance_id, int sync_fd) {
int32_t res = 0; // refer to resErr for meaning
struct wl_display *display;

View File

@ -1,3 +1,3 @@
#include <stdint.h>
int32_t bind_wayland_fd(char *socket_path, int fd, const char *app_id, const char *instance_id, int sync_fd);
int32_t f_bind_wayland_fd(char *socket_path, int fd, const char *app_id, const char *instance_id, int sync_fd);

View File

@ -29,7 +29,7 @@ func bindWaylandFd(socketPath string, fd uintptr, appID, instanceID string, sync
if hasNull(appID) || hasNull(instanceID) {
return ErrContainsNull
}
res := C.bind_wayland_fd(C.CString(socketPath), C.int(fd), C.CString(appID), C.CString(instanceID), C.int(syncFD))
res := C.f_bind_wayland_fd(C.CString(socketPath), C.int(fd), C.CString(appID), C.CString(instanceID), C.int(syncFD))
return resErr[int32(res)]
}