diff options
268 files changed, 10164 insertions, 8181 deletions
diff --git a/.clang-format b/.clang-format index ea649c2b..6a0dea43 100644 --- a/.clang-format +++ b/.clang-format @@ -1,2 +1,119 @@ -ColumnLimit: 0 -IndentWidth: 4 +# SPDX-License-Identifier: GPL-2.0 +# +# clang-format configuration file. Intended for clang-format >= 11. +# +# For more information, see: +# +# Documentation/dev-tools/clang-format.rst +# https://clang.llvm.org/docs/ClangFormat.html +# https://clang.llvm.org/docs/ClangFormatStyleOptions.html +# +--- +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Left +AlignOperands: true +AlignTrailingComments: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: false +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false +BreakBeforeTernaryOperators: false +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeComma +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: false +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 8 +ContinuationIndentWidth: 8 +Cpp11BracedListStyle: false +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: false + +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '.*' + Priority: 1 +IncludeIsMainRegex: '(Test)?$' +IndentCaseLabels: false +IndentGotoLabels: false +IndentPPDirectives: None +IndentWidth: 8 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 8 +ObjCSpaceAfterProperty: true +ObjCSpaceBeforeProtocolList: true + +# Taken from git's rules +PenaltyBreakAssignment: 10 +PenaltyBreakBeforeFirstCallParameter: 30 +PenaltyBreakComment: 10 +PenaltyBreakFirstLessLess: 0 +PenaltyBreakString: 10 +PenaltyExcessCharacter: 100 +PenaltyReturnTypeOnItsOwnLine: 60 + +PointerAlignment: Right +ReflowComments: false +SortIncludes: false +SortUsingDeclarations: false +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatementsExceptForEachMacros +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp03 +TabWidth: 8 +UseTab: Always +... diff --git a/.gitea/README b/.gitea/README new file mode 100644 index 00000000..5a1d49e0 --- /dev/null +++ b/.gitea/README @@ -0,0 +1,5 @@ +This file is generated by 'go generate'. DO NOT EDIT. + +This directory holds workflow documents generated by internal/workflows. Any +changes made here are overwritten on the next 'go generate' call. This setup +avoids having to deal with yaml. diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 134d8bd1..ceee8000 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -1,24 +1 @@ -name: Release - -on: - push: - tags: - - 'v*' - -jobs: - release: - name: Create release - runs-on: nix - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Build for release - run: nix build --print-out-paths --print-build-logs ./test#dist - - - name: Release - uses: https://gitea.com/actions/release-action@main - with: - files: |- - result/hakurei-** - api_key: '${{secrets.RELEASE_TOKEN}}' +{"name":"Release","on":{"push":{"tags":["v*"]}},"jobs":{"release":{"name":"Create release","runs-on":"rosa","steps":[{"name":"Checkout","uses":"actions/checkout@v4","with":{"fetch-depth":0,"fetch-tags":true}},{"name":"Request distribution","id":"dist","run":"/rosa/bin/mbf ci dist -o result . \"$(cat cmd/dist/VERSION)-act\" \u0026\u0026 git tag --list --format='%(contents:body)' \"$(cat cmd/dist/VERSION)\" \u003e\u003e \"/tmp/release-body\""},{"name":"Create release","uses":"https://gitea.com/actions/gitea-release-action@v1.3.7","with":{"files":"result/hakurei-**","body_path":"/tmp/release-body","draft":true}}]}}} diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index d1fc7ef8..639c4f22 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -1,129 +1 @@ -name: Test - -on: - - push - -jobs: - hakurei: - name: Hakurei - runs-on: nix - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Run NixOS test - run: nix build --out-link "result" --print-out-paths --print-build-logs ./test#checks.x86_64-linux.hakurei - - - name: Upload test output - uses: actions/upload-artifact@v3 - with: - name: "hakurei-vm-output" - path: result/* - retention-days: 1 - - race: - name: Hakurei (race detector) - runs-on: nix - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Run NixOS test - run: nix build --out-link "result" --print-out-paths --print-build-logs ./test#checks.x86_64-linux.race - - - name: Upload test output - uses: actions/upload-artifact@v3 - with: - name: "hakurei-race-vm-output" - path: result/* - retention-days: 1 - - sandbox: - name: Sandbox - runs-on: nix - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Run NixOS test - run: nix build --out-link "result" --print-out-paths --print-build-logs ./test#checks.x86_64-linux.sandbox - - - name: Upload test output - uses: actions/upload-artifact@v3 - with: - name: "sandbox-vm-output" - path: result/* - retention-days: 1 - - sandbox-race: - name: Sandbox (race detector) - runs-on: nix - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Run NixOS test - run: nix build --out-link "result" --print-out-paths --print-build-logs ./test#checks.x86_64-linux.sandbox-race - - - name: Upload test output - uses: actions/upload-artifact@v3 - with: - name: "sandbox-race-vm-output" - path: result/* - retention-days: 1 - - sharefs: - name: ShareFS - 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 ./test#checks.x86_64-linux.sharefs - - - name: Upload test output - uses: actions/upload-artifact@v3 - with: - name: "sharefs-vm-output" - path: result/* - retention-days: 1 - - check: - name: Flake checks - needs: - - hakurei - - race - - sandbox - - sandbox-race - - sharefs - 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 ./test - - dist: - name: Create distribution - runs-on: nix - steps: - - name: Checkout - uses: actions/checkout@v4 - - - 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 = /' test/package.nix && - nix build --print-out-paths --print-build-logs ./test#dist && - mv test/{package.nix.old,package.nix} && - echo "rev=$HAKUREI_REV" >> $GITHUB_OUTPUT - - - name: Upload test build - uses: actions/upload-artifact@v3 - with: - name: "hakurei-${{ steps.build-test.outputs.rev }}" - path: result/* - retention-days: 1 +{"name":"Test","on":["push"],"jobs":{"hakurei":{"name":"Hakurei","runs-on":"rosa","steps":[{"name":"Checkout","uses":"actions/checkout@v4"},{"name":"Request distribution","id":"dist","run":"HAKUREI_REV=\"$(git rev-parse --short HEAD)\" \u0026\u0026 /rosa/bin/mbf ci dist -o /tmp/dist . \"$(cat cmd/dist/VERSION)-$HAKUREI_REV\" \u0026\u0026 echo \"rev=$HAKUREI_REV\" \u003e\u003e \"$GITHUB_OUTPUT\""},{"name":"Install hakurei","run":"HAKUREI_VERSION=\"$(cat cmd/dist/VERSION)\" \u0026\u0026 tar -C /tmp/dist -xf /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64.tar.gz \u0026\u0026 /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64/install.sh \u0026\u0026 rm -rf /tmp/dist \u0026\u0026 sudo -u ubuntu hakurei version"},{"name":"Compile and run test suite","run":"unshare -n go run -tags=testsuite ./cmd/hakurei/testsuite"}]},"hakurei-race":{"name":"Hakurei (with race instrument)","runs-on":"rosa","steps":[{"name":"Checkout","uses":"actions/checkout@v4"},{"name":"Request distribution","id":"dist","run":"HAKUREI_REV=\"$(git rev-parse --short HEAD)\" \u0026\u0026 /rosa/bin/mbf ci race -o /tmp/dist . \"$(cat cmd/dist/VERSION)-$HAKUREI_REV\" \u0026\u0026 echo \"rev=$HAKUREI_REV\" \u003e\u003e \"$GITHUB_OUTPUT\""},{"name":"Install hakurei","run":"HAKUREI_VERSION=\"$(cat cmd/dist/VERSION)\" \u0026\u0026 tar -C /tmp/dist -xf /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64.tar.gz \u0026\u0026 /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64/install.sh \u0026\u0026 rm -rf /tmp/dist \u0026\u0026 sudo -u ubuntu hakurei version"},{"name":"Compile and run test suite","run":"unshare -n go run -tags=testsuite ./cmd/hakurei/testsuite"}]},"sandbox":{"name":"Sandbox","runs-on":"rosa","steps":[{"name":"Checkout","uses":"actions/checkout@v4"},{"name":"Request distribution","id":"dist","run":"HAKUREI_REV=\"$(git rev-parse --short HEAD)\" \u0026\u0026 /rosa/bin/mbf ci dist -o /tmp/dist . \"$(cat cmd/dist/VERSION)-$HAKUREI_REV\" \u0026\u0026 echo \"rev=$HAKUREI_REV\" \u003e\u003e \"$GITHUB_OUTPUT\""},{"name":"Install hakurei","run":"HAKUREI_VERSION=\"$(cat cmd/dist/VERSION)\" \u0026\u0026 tar -C /tmp/dist -xf /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64.tar.gz \u0026\u0026 /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64/install.sh \u0026\u0026 rm -rf /tmp/dist \u0026\u0026 sudo -u ubuntu hakurei version"},{"name":"Compile and run test suite","run":"unshare -n go run -tags=testsuite ./cmd/hakurei/testsuite/sandbox"}]},"sandbox-race":{"name":"Sandbox (with race instrument)","runs-on":"rosa","steps":[{"name":"Checkout","uses":"actions/checkout@v4"},{"name":"Request distribution","id":"dist","run":"HAKUREI_REV=\"$(git rev-parse --short HEAD)\" \u0026\u0026 /rosa/bin/mbf ci race -o /tmp/dist . \"$(cat cmd/dist/VERSION)-$HAKUREI_REV\" \u0026\u0026 echo \"rev=$HAKUREI_REV\" \u003e\u003e \"$GITHUB_OUTPUT\""},{"name":"Install hakurei","run":"HAKUREI_VERSION=\"$(cat cmd/dist/VERSION)\" \u0026\u0026 tar -C /tmp/dist -xf /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64.tar.gz \u0026\u0026 /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64/install.sh \u0026\u0026 rm -rf /tmp/dist \u0026\u0026 sudo -u ubuntu hakurei version"},{"name":"Compile and run test suite","run":"unshare -n go run -tags=testsuite ./cmd/hakurei/testsuite/sandbox"}]},"sharefs":{"name":"ShareFS","runs-on":"rosa","steps":[{"name":"Checkout","uses":"actions/checkout@v4"},{"name":"Request distribution","id":"dist","run":"HAKUREI_REV=\"$(git rev-parse --short HEAD)\" \u0026\u0026 /rosa/bin/mbf ci dist -o /tmp/dist . \"$(cat cmd/dist/VERSION)-$HAKUREI_REV\" \u0026\u0026 echo \"rev=$HAKUREI_REV\" \u003e\u003e \"$GITHUB_OUTPUT\""},{"name":"Install hakurei","run":"HAKUREI_VERSION=\"$(cat cmd/dist/VERSION)\" \u0026\u0026 tar -C /tmp/dist -xf /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64.tar.gz \u0026\u0026 /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64/install.sh \u0026\u0026 rm -rf /tmp/dist \u0026\u0026 sudo -u ubuntu hakurei version"},{"name":"Mount sharefs","run":"useradd -ru 1023 -md /var/lib/sdcard -k /var/empty -s /sbin/nologin media_rw \u0026\u0026 install -dm0 /sdcard \u0026\u0026 sharefs -o rw,noexec,nosuid,nodev,noatime,allow_other,mkdir,source=/var/lib/sdcard,setuid=1023,setgid=1023 /sdcard"},{"name":"Compile and run test suite","run":"sharefs -V \u0026\u0026 go run -tags=testsuite ./cmd/sharefs/testsuite"},{"name":"Upload test output","uses":"actions/upload-artifact@v3","with":{"name":"fs_mark","path":"result/*","retention-days":1}}]}}} @@ -8,8 +8,8 @@ # go generate /cmd/hakurei/LICENSE -/cmd/mbf/internal/pkgserver/ui/static -/pkg/internal/testtool/testtool +/cmd/mbf/internal/pkgsite/ui/static +/pkg/internal/tester/tester /internal/rosa/hakurei_current.tar.gz # cmd/dist default destination @@ -1,4 +1,4 @@ -Copyright (c) 2024-2025 Ophestra +Copyright (c) 2024-2026 Rosa OS Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: @@ -1,19 +1,6 @@ -<p align="center"> - <a href="https://git.gensokyo.uk/rosa/hakurei"> - <picture> - <img src="https://basement.gensokyo.uk/images/yukari1.png" width="200px" alt="Yukari"> - </picture> - </a> -</p> - -<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://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> - <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/rosa/hakurei/src/branch/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-pink" alt="MIT License" /></a> - <a href="https://hakurei.app"><img src="https://img.shields.io/website?url=https%3A%2F%2Fhakurei.app" alt="Website" /></a> -</p> +<p align="center"><a href="https://src.rosa.moe/hakurei"><picture> + <img src="https://basement.gensokyo.uk/images/yukari1.png" width="200px" alt="Yukari"> +</picture></a></p> Hakurei is a tool for running sandboxed desktop applications as dedicated subordinate users on the Linux kernel. It implements the application container @@ -24,6 +11,9 @@ Interaction with hakurei happens entirely through structures described by package [hst](https://pkg.go.dev/hakurei.app/hst). No native API is available due to internal details of uid isolation. +Our canonical Git repository is located at https://src.rosa.moe/hakurei. +There is a mirror of the repository https://git.gensokyo.uk/rosa/hakurei. + ## Notable Packages Package [container](https://pkg.go.dev/hakurei.app/container) is general purpose diff --git a/cmd/app/app.go b/cmd/app/app.go index 9679413f..ca809334 100644 --- a/cmd/app/app.go +++ b/cmd/app/app.go @@ -14,6 +14,24 @@ import ( "hakurei.app/hst" ) +// base is the root of the persistent state directory. +type base check.Absolute + +// append calls filepath.Join with b as the first element. +func (b *base) append(elem ...string) *check.Absolute { + return (*check.Absolute)(b).Append(elem...) +} + +// initial returns the pathname of the bottom layer. +func (b *base) initial() *check.Absolute { + return b.append("initial") +} + +// state returns the pathname of the home directory for id. +func (b *base) state(id string) *check.Absolute { + return b.append("state", id) +} + // parsePair parses a NUL-delimited quoted paths pair. func parsePair(s string) (source, target *check.Absolute, err error) { var p string @@ -35,7 +53,7 @@ func parsePair(s string) (source, target *check.Absolute, err error) { // corresponding [hst.Config]. func parse( id string, - base *check.Absolute, + base *base, r io.Reader, templateP *string, ) (*hst.Config, error) { @@ -44,7 +62,7 @@ func parse( root := hst.FSOverlay{ Target: fhs.AbsRoot, - Lower: []*check.Absolute{base.Append("initial")}, + Lower: []*check.Absolute{base.initial()}, } c := hst.Config{ ID: id, @@ -65,7 +83,7 @@ func parse( {FilesystemConfig: &root}, {FilesystemConfig: &hst.FSBind{ Target: home, - Source: base.Append("state", id), + Source: base.state(id), Write: true, Ensure: true, }}, @@ -110,7 +128,7 @@ func parse( *templateP = template } c.Identity = v - root.Upper = base.Append("template", template) + root.Upper = base.template(template) } if err := scanOnce(); err != nil { diff --git a/cmd/app/app_merged.go b/cmd/app/app_merged.go new file mode 100644 index 00000000..e0a31f94 --- /dev/null +++ b/cmd/app/app_merged.go @@ -0,0 +1,15 @@ +//go:build merge + +package main + +import "hakurei.app/check" + +// template returns the pathname of an upperdir. +func (b *base) template(template string) *check.Absolute { + return b.append("template", template, "upper") +} + +// work returns the pathname of a workdir. +func (b *base) work(template string) *check.Absolute { + return b.append("template", template, "work") +} diff --git a/cmd/app/app_sep.go b/cmd/app/app_sep.go new file mode 100644 index 00000000..9ec98e41 --- /dev/null +++ b/cmd/app/app_sep.go @@ -0,0 +1,15 @@ +//go:build !merge + +package main + +import "hakurei.app/check" + +// template returns the pathname of an upperdir. +func (b *base) template(template string) *check.Absolute { + return b.append("template", template) +} + +// work returns the pathname of a workdir. +func (b *base) work(template string) *check.Absolute { + return b.append("work", template) +} diff --git a/cmd/app/app_test.go b/cmd/app/app_test.go index 1aab4759..90c0068e 100644 --- a/cmd/app/app_test.go +++ b/cmd/app/app_test.go @@ -13,7 +13,7 @@ import ( func TestParse(t *testing.T) { t.Parallel() - base := fhs.AbsProc.Append("nonexistent") + b := (*base)(fhs.AbsProc.Append("nonexistent")) testCases := []struct { name string data string @@ -74,13 +74,13 @@ talk com.canonical.Unity {FilesystemConfig: &hst.FSOverlay{ Target: fhs.AbsRoot, Lower: []*check.Absolute{ - base.Append("initial"), + b.initial(), }, - Upper: base.Append("template", "nonfree"), + Upper: b.template("nonfree"), }}, {FilesystemConfig: &hst.FSBind{ Target: hst.AbsPrivateTmp.Append("home"), - Source: base.Append("state", "com.discordapp.Discord"), + Source: b.state("com.discordapp.Discord"), Write: true, Ensure: true, }}, @@ -128,7 +128,7 @@ talk com.canonical.Unity got, err := parse( tc.name, - base, + b, strings.NewReader(tc.data), nil, ) diff --git a/cmd/app/doc.go b/cmd/app/doc.go new file mode 100644 index 00000000..8c5397fd --- /dev/null +++ b/cmd/app/doc.go @@ -0,0 +1,362 @@ +/* +The app program is a proof-of-concept frontend for cmd/hakurei. + +This program is not covered by the compatibility promise. The command line +interface and configuration syntax may change at any time. + +# Installation + +Compile cmd/app with these arguments: + + go build -trimpath \ + -ldflags='-s -w + -buildid= + -X hakurei.app/internal/info.hsuPath=/usr/local/bin/hsu' \ + ./cmd/app + +Replace /usr/local/bin/hsu with the hakurei installation's absolute hsu +pathname. + +# Sharing files + +Sharing files between apps is only possible with the permissionless shared +filesystem ([cmd/sharefs]), as apps generally do not share credentials. It can +be built without any special linker flags. Create a symlink at +/usr/bin/mount.fuse.sharefs pointing to the sharefs binary when mounting sharefs +via fstab: + + sharefs /sdcard fuse.sharefs rw,noexec,nosuid,nodev,noatime,allow_other,mkdir,source=/var/lib/sdcard,setuid=1023,setgid=1023 0 0 + +Replace /var/lib/sdcard with the location of the backing directory. User and +group id must be specified in numerical form, it must own the backing directory. +The mount point does not have to be /sdcard. + +# General setup + +The entire directory structure containing persistent app data must be created +manually for now, due to the different ownership requirements being impossible +to set up directly through cmd/hsu. + +The environment variable ROSA_APP_PATH should be set to the absolute pathname of +the persistent state directory. This document will use $ROSA_APP_PATH to refer +to the persistent state directory. The $ROSA_APP_PATH directory should be owned +by the user declared in /etc/hsurc and invoking cmd/app, and must be readable +and executable by all subordinate users. This can be done by making it +world-readable and executable, or by adding a group directive to the common +file. + +The $ROSA_APP_PATH/app directory contains app configuration files, named after +their reverse-DNS style application identifier string. It should be owned by +the user invoking cmd/app. + +The $ROSA_APP_PATH/initial directory contains the read-only base of every +app template. Everything within it, including the directory itself, should be +owned by the reserved user, its user and group id obtained by the command: + + app id + +This document assumes a [Void Linux rootfs tarball] is unpacked here. It is +possible to use other Linux distributions; Void linux is selected here because +its package manager works correctly out of the box in the hakurei container +environment. If the use case does not require glibc, [Alpine Linux] or +[Chimera Linux] might be more suitable. + +Once the tarball is unpacked at $ROSA_APP_PATH/initial, create a directory named +chronos in $ROSA_APP_PATH/initial/home, and directories block, bus, class, dev, +and devices in $ROSA_APP_PATH/initial/sys. Then, recursively change ownerships +of files and directories in $ROSA_APP_PATH/initial to the reserved user/group. + +The $ROSA_APP_PATH/lock directory contains lock files guarding entry into +mutable and app containers of every template. It must be owned by the user +invoking cmd/app. + +The $ROSA_APP_PATH/state directory contains app home directories, named after +their reverse-DNS style application identifier string. The directory itself +should be owned by the user invoking cmd/app; each directory inside must be +owned by the subordinate user/group id of its app, obtained by the command: + + app id "$APPID" + +where "$APPID" is the second component of the first line in the configuration +file of the corresponding app. It is considered good practice, but not required, +for these directory to have the permission bits set to 0700. + +The $ROSA_APP_PATH/template directory contains templates that apps are based +on, which are all derived from the initial layer. The directory itself +should be owned by the user invoking cmd/app, and each directory inside should +be owned by the reserved user/group. Their names are used in the first component +of the first line in the configuration file of each app, to specify that the +file is derived from that template. + +The $ROSA_APP_PATH/work directory contains overlay work directories for mutable +containers. It must contain one manually created, empty directory for each +template, named after the template directory itself, owned by the reserved +user/group. The directory itself and its contents should have the permission +bits set to 0700. + +# Snapshots and cloning + +To reduce disk space used by templates, [ZFS clones] or an equivalent can be +used. In such a setup, where each template occupies a different filesystem, the +"merge" build tag is required, and the work directory can be omitted during +directory creation. Instead, the individual template directories contain both +upper and work directories. + +# Configuring the base template + +This section describes basic setup required for the typical desktop use case. +Generally, multiple templates are created to mitigate the global nature of +conventional package managers. The setup described in this section applies to +all templates. For convenience, a "base" template should be created, containing +this setup, and all future templates should be copied from the base template. +This section assumes the template is named "base". + +Create an empty directory at $ROSA_APP_PATH/template/base, owned by the reserved +user, with permission bits set to 0755. Create its corresponding work directory +at $ROSA_APP_PATH/work/base, also owned by the reserved user, with permission +bits set to 0700. Once this is complete, enter its mutable container with the +command: + + app enter --shell=/bin/sh base + +This command overrides the container configuration to use the shell program at +/bin/sh. Normally, cmd/app uses zsh, which is not yet installed. + +Once in the container, populate /etc/resolv.conf with some well-known DNS +service, for example: + + nameserver 8.8.8.8 + nameserver 8.8.4.4 + +The mutable container does not bind the host /etc/resolv.conf. It is generally +recommended to populate it with a well-known service here to avoid trouble in +future template changes. A later section configures regular app containers to +use host configuration. + +After populating nameserver configuration, install zsh using the package manager +provided by the distribution unpacked earlier. On Void Linux, this is done by +the command: + + xbps-install zsh + +Wait for the package installation to complete, configure zsh if necessary, then +exit from the shell, and re-enter the container without overriding the login +shell: + + app enter base + +Some packages are generally required in the container for the typical desktop +use case: xdg-user-dirs to reconfigure "well known" user directories to point +to the inner sharefs mount point, mesa to interact with the GPU, dconf to +configure GTK, xdg-desktop-portal-gtk to have gtk talk to dconf. Fonts generally +need to be installed as well. Additionally, a text editor can be installed for +editing configuration files later on. On Void Linux, these packages are +installed by the command: + + xbps-install \ + vim \ + mesa \ + mesa-dri \ + mesa-intel-dri \ + mesa-vaapi \ + mesa-vulkan-intel \ + mesa-vulkan-radeon \ + mesa-vulkan-overlay-layer \ + dconf \ + xdg-user-dirs \ + xdg-desktop-portal-gtk \ + noto-fonts-ttf \ + noto-fonts-ttf-variable \ + noto-fonts-ttf-extra \ + noto-fonts-emoji \ + noto-fonts-cjk \ + noto-fonts-cjk-variable \ + noto-fonts-cjk-sans \ + noto-fonts-cjk-sans-variable \ + noto-fonts-cjk-serif \ + noto-fonts-cjk-serif-variable + +Add -32bit variants of mesa packages if multilib support is required. Adjust +the package selection based on use case. + +Edit /etc/xdg/user-dirs.defaults and point directories to the inner sharefs +mount point as required. The resulting file should look like this: + + # Default settings for user directories + # + # The values are relative pathnames from the home directory and + # will be translated on a per-path-element basis into the users locale + DESKTOP=../../sdcard/Desktop + DOWNLOAD=../../sdcard/Download + TEMPLATES=../../sdcard/Templates + PUBLICSHARE=../../sdcard/Public + DOCUMENTS=../../sdcard/Documents + MUSIC=../../sdcard/Music + PICTURES=../../sdcard/Pictures + VIDEOS=../../sdcard/Movies + PROJECTS=../../sdcard/Projects + # Another alternative is: + #MUSIC=Documents/Music + #PICTURES=Documents/Pictures + #VIDEOS=Documents/Videos + +If this is configured, it must be applied to the home directory of each app +individually. This can be done for all apps via the shell expression: + + app run | xargs -n 1 echo app run --command=xdg-user-dirs-update + +This must also be done for every newly created app. + +The /etc/dconf directory can be set up to provide defaults across all apps. To +do so, edit /etc/dconf/profile/user: + + user-db:user + system-db:local + system-db:site + system-db:distro + +Then, place files in /etc/dconf/db/local.d containing dconf configuration. For +example: + + [org/gnome/desktop/interface] + gtk-enable-primary-paste=true + color-scheme='prefer-dark' + gtk-theme='adw-gtk3-dark' + icon-theme='Papirus-Dark' + +Themes must be installed in the container. Change colour-scheme and themes +accordingly. Setting gtk-enable-primary-paste restores clipboard behaviour that +GNOME maintainers decided to break. + +After editing these configuration files, update dconf system databases: + + dconf update + +# Common configuration + +The optional $ROSA_APP_PATH/common file contains common configuration included +after the specific configuration of each app. Some bind mounts are required for +almost every graphical program, and many widely used libraries are configured +through the environment. For most setups, these directives are generally +required: + + ; for libudev + ro "/sys/block" + ro "/sys/bus" + ro "/sys/class" + ro "/sys/dev" + ro "/sys/devices" + + env EDITOR=vim + ; for apps to play nice with xdg-dbus-proxy + ro+ "/etc/machine-id" + ; template must have a /etc/resolv.conf file + ro+ "/etc/resolv.conf" + ; group name of the sharefs group configured on host + group media_rw + ; for sharefs + rw "/sdcard" + ; refer to /usr/share/zoneinfo + env TZ=Asia/Tokyo + + ; must be installed first + env XCURSOR_THEME=volantes_cursors + ; must be generated first if using glibc + env LANG=en_GB.UTF-8 + env LC_COLLATE=C + +# Installing an app + +An app is defined by a configuration file in $ROSA_APP_PATH/app, and a +persistent state directory in $ROSA_APP_PATH/state. Before creating an app, its +identity must be decided. Different apps should generally not share an identity. +The next unused identity can be found using the command: + + app next + +If the -v argument is added before the "next" command, cmd/app will additionally +show apps sharing the same identity. + +Once the identity is decided, the subordinate user/group id can be obtained by +the command: + + app id "$APPID" + +where "$APPID" is the identity of this app. A directory must be created under +$ROSA_APP_PATH/state, owned by this user and group id. Its permission bits +should be set to 0700. A configuration file with the same name, owned by the +user invoking cmd/app, must be placed in $ROSA_APP_PATH/app. Its contents are +described in the next section. The reverse-DNS style application identifier +string is submitted to the Wayland display server, and used as part of the +dbus preset if enabled, so the correct identifier must be obtained. If no such +identifier exist, use the domain of the app home page. + +# Configuring an app + +The configuration file starts with two structural directives, and the remaining +lines are freestanding directives applied in order. + +The first structural directive is a line containing two components separated by +the ':' byte. The first component is the template used by the app. the second +component is the decimal representation of the app identity. The second +structural directive is a shell expression passed to the shell serving as the +initial process of the app. + +After the structural directives, each line contains exactly one directive or +comment. Comment lines begin with a ';' byte: these lines are not interpreted by +cmd/app in any way. + +The following section documents currently available freestanding directives. + +# Directives + + interactive start initial process as an interactive shell + gpu expose GPU devices to the container + system_bus enable system bus in the dbus proxy + + wayland expose a Wayland pathname socket via security-context-v1 + x11 expose the X11 pathname socket + dbus enable the per-container xdg-dbus-proxy daemon + pipewire expose a pipewire pathname socket via SecurityContext + + multiarch unblock system calls required for multiarch to work on + multiarch-enabled targets (amd64, arm64) + devel unblock ptrace and friends + userns unblock userns creation and container setup syscalls + net enable network access + abstract enable access to external abstract unix sockets + tty unblock dangerous terminal I/O (faking input) + mapuid map the target user id to the user id of the user + invoking cmd/app in the container user namespace + device mount /dev/ from the init mount namespace as is in the + container mount namespace + + share_runtime share XDG_RUNTIME_DIR between containers under the same + identity + share_tmpdir share TMPDIR between containers under the same identity + + username <name> set username of the emulated user + hostname <name> set container hostname + env KEY=VALUE set an environment variable for the initial process + + ro "pathname" make a host path available to the container; the string + must be presented in Go string literal syntax, to + specify a different inner pathname, end the outer + pathname with NUL and specify the inner pathname after + the NUL byte + rw "pathname" like ro, but the resulting mount entry is made writable + ro+ "pathname" like ro, but is skipped if pathname does not exist + rw+ "pathname" like ro+, but the resulting mount entry is made writable + + own name add an own policy for the dbus proxy + own_system name like own, but for the system bus if enabled + talk name add a talk policy for the dbus proxy + talk_system name like talk, but for the system bus if enabled + +[cmd/sharefs]: https://pkg.go.dev/hakurei.app/cmd/sharefs +[Void Linux rootfs tarball]: https://voidlinux.org/download +[Alpine Linux]: https://alpinelinux.org +[Chimera Linux]: https://chimera-linux.org +[ZFS clones]: https://openzfs.github.io/openzfs-docs/man/v2.4/8/zfs-clone.8.html +*/ +package main diff --git a/cmd/app/lock.go b/cmd/app/lock.go index 88068381..2a419ea1 100644 --- a/cmd/app/lock.go +++ b/cmd/app/lock.go @@ -8,7 +8,6 @@ import ( "strings" "syscall" - "hakurei.app/check" "hakurei.app/fhs" "hakurei.app/hst" "hakurei.app/internal/env" @@ -24,15 +23,15 @@ func (e MutationConflictError) Error() string { } // informTemplate guards intention of a template or its derivatives. -func informTemplate(base *check.Absolute, name string, mutable bool) (func() error, error) { - mu := lockedfile.MutexAt(base.Append("lock", name).String()) +func informTemplate(b *base, name string, mutable bool) (func() error, error) { + mu := lockedfile.MutexAt(b.append("lock", name).String()) if unlock, err := mu.Lock(); err != nil { return nil, err } else { defer unlock() } - marker := base.Append("lock", "."+name) + marker := b.append("lock", "."+name) if p, err := os.ReadFile(marker.String()); err == nil { if _, err = os.Stat(fhs.AbsProc.Append(string(p)).String()); err == nil { return nil, MutationConflictError(p) @@ -72,8 +71,8 @@ func informTemplate(base *check.Absolute, name string, mutable bool) (func() err if !root.Target.Is(fhs.AbsRoot) || len(root.Lower) != 1 || - !root.Lower[0].Is(base.Append("initial")) || - !root.Upper.Is(base.Append("template", name)) || + !root.Lower[0].Is(b.initial()) || + !root.Upper.Is(b.template(name)) || root.Work != nil { continue } @@ -102,12 +101,12 @@ func informTemplate(base *check.Absolute, name string, mutable bool) (func() err } // acquireTemplate obtains exclusivity of a template. -func acquireTemplate(base *check.Absolute, name string) (remove func() error, err error) { - return informTemplate(base, name, true) +func acquireTemplate(b *base, name string) (remove func() error, err error) { + return informTemplate(b, name, true) } // enterTemplate checks against exclusivity of a template. -func enterTemplate(base *check.Absolute, name string) error { - _, err := informTemplate(base, name, false) +func enterTemplate(b *base, name string) error { + _, err := informTemplate(b, name, false) return err } diff --git a/cmd/app/main.go b/cmd/app/main.go index 00bceb57..9b51d601 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -1,24 +1,25 @@ -// The app program is a proof-of-concept frontend for cmd/hakurei. -// -// This program is not covered by the compatibility promise. The command line -// interface and configuration syntax may change at any time. package main import ( "context" "errors" + "fmt" "io" "log" "os" "os/exec" "os/signal" "path/filepath" + "slices" + "strconv" + "strings" "syscall" "hakurei.app/check" "hakurei.app/command" "hakurei.app/fhs" "hakurei.app/hst" + "hakurei.app/internal/outcome" "hakurei.app/message" ) @@ -36,7 +37,7 @@ func main() { flagBase string flagInsecure bool - base, template, initial *check.Absolute + b *base ) c := command.New(os.Stderr, log.Printf, "app", func([]string) (err error) { msg.SwapVerbose(flagVerbose) @@ -44,14 +45,15 @@ func main() { if flagBase == "" { flagBase = "state" } + + var a *check.Absolute if flagBase, err = filepath.Abs(flagBase); err != nil { return - } else if base, err = check.NewAbs(flagBase); err != nil { + } else if a, err = check.NewAbs(flagBase); err != nil { return } + b = (*base)(a) - template = base.Append("template") - initial = base.Append("initial") return }).Flag( &flagVerbose, @@ -76,7 +78,7 @@ func main() { "enter", "Enter mutable state template", func(args []string) error { if len(args) != 1 { - return list(template, true) + return list(b.append("template"), true) } config := hst.Config{ @@ -86,9 +88,9 @@ func main() { Filesystem: []hst.FilesystemConfigJSON{ {FilesystemConfig: &hst.FSOverlay{ Target: fhs.AbsRoot, - Lower: []*check.Absolute{initial}, - Upper: template.Append(args[0]), - Work: base.Append("work", args[0]), + Lower: []*check.Absolute{b.initial()}, + Upper: b.template(args[0]), + Work: b.work(args[0]), }}, {FilesystemConfig: &hst.FSEphemeral{ Target: fhs.AbsTmp, @@ -122,7 +124,7 @@ func main() { config.Container.Home = a } - remove, err := acquireTemplate(base, args[0]) + remove, err := acquireTemplate(b, args[0]) if err != nil { return err } @@ -148,19 +150,19 @@ func main() { "run", "Start the named application", func(args []string) error { if len(args) < 1 { - return list(base.Append("app"), false) + return list(b.append("app"), false) } var config *hst.Config var r io.Reader - f, err := os.Open(base.Append("app", args[0]).String()) + f, err := os.Open(b.append("app", args[0]).String()) if err != nil { return err } r = f var common *os.File - if common, err = os.Open(base.Append("common").String()); err != nil { + if common, err = os.Open(b.append("common").String()); err != nil { if !errors.Is(err, os.ErrNotExist) { _ = f.Close() return err @@ -170,7 +172,7 @@ func main() { } var name string - config, err = parse(args[0], base, r, &name) + config, err = parse(args[0], b, r, &name) if closeErr := f.Close(); err == nil { err = closeErr } @@ -187,7 +189,7 @@ func main() { config.Container.Args[2] = flagCommand } - if err = enterTemplate(base, name); err != nil { + if err = enterTemplate(b, name); err != nil { return err } return run(ctx, msg, flagInsecure, config, args[1:]...) @@ -200,6 +202,105 @@ func main() { ) } + c.NewCommand( + "id", "Show user/group id of the specified appid", + func(args []string) error { + var appid int + switch len(args) { + case 0: + log.Println("appid not specified, assuming reserved user") + break + + case 1: + var err error + appid, err = strconv.Atoi(args[0]) + if err != nil { + return os.ErrInvalid + } + break + + default: + return errors.New("id requires 1 argument") + } + + fmt.Println(hst.ToUser(outcome.Info().User, appid)) + return nil + }, + ) + + c.NewCommand( + "next", "Find next unused identity", + func([]string) error { + var names []string + if dents, err := os.ReadDir(b.append("app").String()); err != nil { + return err + } else { + names = make([]string, 0, len(dents)) + for _, dent := range dents { + name := dent.Name() + if dent.IsDir() || (len(name) > 0 && name[0] == '.') { + continue + } + names = append(names, name) + } + } + + apps := make([]*hst.Config, len(names)) + for i, name := range names { + r, err := os.Open(b.append("app", name).String()) + if err != nil { + return err + } + + apps[i], err = parse(name, b, r, nil) + if closeErr := r.Close(); err == nil { + err = closeErr + } + if err != nil { + return err + } + } + + p := make(map[int][]*hst.Config) + for _, config := range apps { + p[config.Identity] = append(p[config.Identity], config) + } + + identities := make([]int, 0, len(p)) + for identity, a := range p { + identities = append(identities, identity) + if msg.IsVerbose() && len(a) != 1 { + ids := make([]string, len(a)) + for i, config := range a { + ids[i] = config.ID + } + slices.Sort(ids) + msg.Verbosef( + "%s shares identity %d", + strings.Join(ids, ", "), identity, + ) + } + } + + if len(identities) == 0 { + // 0 is the reserved identity + fmt.Println(1) + return nil + } + + slices.Sort(identities) + next := identities[0] - 1 + for _, identity := range identities { + if identity != next+1 { + break + } + next = identity + } + fmt.Println(next + 1) + return nil + }, + ) + c.MustParse(os.Args[1:], func(err error) { if e, ok := errors.AsType[*exec.ExitError](err); ok && e != nil { os.Exit(e.ExitCode()) diff --git a/cmd/dist/VERSION b/cmd/dist/VERSION index f10bec9e..4ea4ea46 100644 --- a/cmd/dist/VERSION +++ b/cmd/dist/VERSION @@ -1 +1 @@ -v0.4.8 +v0.4.9 diff --git a/cmd/dist/main.go b/cmd/dist/main.go index 8e1c57c1..40a0583c 100644 --- a/cmd/dist/main.go +++ b/cmd/dist/main.go @@ -4,6 +4,7 @@ package main import ( "archive/tar" + "bytes" "compress/gzip" "context" "crypto/sha512" @@ -56,6 +57,8 @@ func main() { verbose := os.Getenv("VERBOSE") != "" runTests := os.Getenv("HAKUREI_DIST_MAKE") == "" + race := os.Getenv("HAKUREI_RACE") != "" + dynamic := os.Getenv("HAKUREI_DYNAMIC") != "" version = getenv("HAKUREI_VERSION", strings.TrimSpace(version)) prefix := getenv("PREFIX", "/usr/local") destdir := getenv("DESTDIR", "dist") @@ -64,6 +67,16 @@ func main() { log.Println() } + if race { + version += "-race" + } + + if err := os.Setenv("CC", "clang"); err != nil { + log.Fatal(err) + } else if err = os.Setenv("CXX", "clang++"); err != nil { + log.Fatal(err) + } + if err := os.MkdirAll(destdir, 0755); err != nil { log.Fatal(err) } @@ -89,10 +102,36 @@ func main() { ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) defer cancel() + const stubFlag = "-buildvcs=false" verboseFlag := "-v" if !verbose { - verboseFlag = "-buildvcs=false" + verboseFlag = stubFlag + } + + raceFlag := "-race" + if !race { + raceFlag = stubFlag + } + + ldflagsStatic := " -extldflags=-static" + if dynamic { + cmd := exec.CommandContext(ctx, "llvm-config", "--host-target") + cmd.Stderr = os.Stderr + var p []byte + p, err = cmd.Output() + if err != nil { + log.Fatal(err) + } + p = bytes.TrimSpace(p) + _, abi, ok := strings.CutLast(string(p), "-") + if !ok { + log.Fatalf("invalid target %q", string(p)) + } + version += "-" + abi + + log.Println("WARNING: dynamic linking is not supported") + ldflagsStatic = "" } log.Printf("Building hakurei %s for %s/%s.", version, runtime.GOOS, runtime.GOARCH) @@ -100,9 +139,9 @@ func main() { mustRun( ctx, nil, "go", "build", "-trimpath", - verboseFlag, "-o", s, + verboseFlag, raceFlag, "-o", s, "-ldflags=-s -w "+ - "-buildid= -linkmode external -extldflags=-static "+ + "-buildid= -linkmode external"+ldflagsStatic+" "+ "-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", @@ -125,8 +164,8 @@ func main() { if runTests { log.Println("##### Testing Hakurei.") mustRun( - ctx, nil, "go", "test", - "-ldflags=-buildid= -linkmode external -extldflags=-static", + ctx, nil, "go", "test", raceFlag, + "-ldflags=-buildid= -linkmode external"+ldflagsStatic, "./...", ) log.Println() diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go index 76004d4f..f14945ad 100644 --- a/cmd/hakurei/command.go +++ b/cmd/hakurei/command.go @@ -106,7 +106,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr flagSchedPolicy string flagSchedPriority int - flagPrivateRuntime, flagPrivateTmpdir bool + flagNoWait, flagPrivateRuntime, flagPrivateTmpdir bool flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool ) @@ -194,6 +194,10 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr }, } + if flagNoWait { + config.Container.AdoptWaitDelay = -1 + } + if err := config.SchedPolicy.UnmarshalText( []byte(flagSchedPolicy), ); err != nil { @@ -315,6 +319,8 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr "Scheduling policy to set for the container"). Flag(&flagSchedPriority, "priority", command.IntFlag(0), "Scheduling priority to set for the container"). + Flag(&flagNoWait, "no-wait", command.BoolFlag(false), + "Do not wait for lingering processes to terminate"). 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), diff --git a/cmd/hakurei/command_test.go b/cmd/hakurei/command_test.go index 059e18b1..49a1f9d4 100644 --- a/cmd/hakurei/command_test.go +++ b/cmd/hakurei/command_test.go @@ -39,8 +39,8 @@ commands: [--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> [<args>] + [--no-wait] [--private-runtime] [--private-tmpdir] + [--wayland] [-X] [--dbus] [--pipewire] [--pulse] <command> [<args>] flags: -X Enable direct connection to X11 @@ -62,6 +62,8 @@ flags: Reverse-DNS style Application identifier, leave empty to inherit instance identifier -mpris Allow owning MPRIS D-Bus path, has no effect if custom config is available + -no-wait + Do not wait for lingering processes to terminate -pipewire Enable connection to PipeWire via SecurityContext -policy string diff --git a/cmd/hakurei/testsuite/main.go b/cmd/hakurei/testsuite/main.go new file mode 100644 index 00000000..0e031430 --- /dev/null +++ b/cmd/hakurei/testsuite/main.go @@ -0,0 +1,900 @@ +//go:build testsuite + +// The hakurei test program tests cmd/hakurei behaviour against a graphical +// session and applications simulating a desktop workload. +package main + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "io/fs" + "log" + "os" + "os/exec" + "path/filepath" + "slices" + "strconv" + "strings" + "sync" + "syscall" + "time" + "unsafe" + + "hakurei.app/check" + "hakurei.app/ext" + "hakurei.app/fhs" + "hakurei.app/hst" + "hakurei.app/internal/testsuite" +) + +// newConfig returns the address of a new bare minimal hakurei configuration. +func newConfig( + name string, + file string, + args ...string, +) *hst.Config { + c := hst.Config{ + ID: "app.hakurei." + name, + Identity: int(testsuite.NewSerial() % hst.IdentityEnd), + + Container: &hst.ContainerConfig{ + Hostname: name, + Filesystem: []hst.FilesystemConfigJSON{ + {FilesystemConfig: &hst.FSBind{ + Target: fhs.AbsRoot, + Source: fhs.AbsRoot, + Special: true, + }}, + }, + Username: "nobody", + Shell: fhs.AbsUsrBin.Append("bash"), + Home: hst.AbsPrivateTmp, + }, + } + + if p, err := exec.LookPath(file); err != nil { + log.Fatal(err) + } else if p, err = filepath.Abs(p); err != nil { + log.Fatal(err) + } else { + c.Container.Path = check.MustAbs(p) + } + c.Container.Args = []string{filepath.Base(file)} + c.Container.Args = append(c.Container.Args, args...) + return &c +} + +// checkHakurei starts a hakurei container and calls f. +func checkHakurei( + ctx context.Context, + wg *sync.WaitGroup, + cred *syscall.Credential, + dbusEnv string, + + name, long string, + scan, verbose, insecure bool, + retry int, + processConfig func(c *hst.Config), + f func(p hst.State, pid int) error, + + checkOutput func(output string) bool, + wantCode ...int, +) { + if !scan && f != nil { + log.Fatalf("(%s) cannot inspect without scanning", name) + } + if checkOutput == nil { + checkOutput = func(output string) bool { return output == "" } + } + if len(wantCode) == 0 { + wantCode = []int{130} + } + + c := newConfig(name, "sleep", "infinity") + if processConfig != nil { + processConfig(c) + } + + wg.Go(func() { + p, pid := testsuite.MustStartHakurei( + ctx, wg, cred, dbusEnv, func(err error, output string) { + if err == nil { + if !slices.Contains(wantCode, 0) { + if retry > 0 { + goto retry + } + log.Fatalf( + "%s test process unexpectedly succeeded\n%s", + long, output, + ) + } + } else if e, ok := errors.AsType[*exec.ExitError](err); !ok { + // problem with starting cmd/hakurei, not retryable + log.Fatalf("%s: %v\n%s", long, err, output) + } else if !slices.Contains(wantCode, e.ExitCode()) { + if retry > 0 { + goto retry + } + log.Fatalf("%s: %v\n%s", long, e, output) + } + + if !checkOutput(output) { + if retry > 0 { + goto retry + } + log.Fatalf("%s: %q", long, output) + } + + log.Println("validated", long) + return + + retry: + time.Sleep(100 * time.Millisecond) + log.Printf("retrying %s", name) + checkHakurei( + ctx, wg, cred, dbusEnv, + name, long, + scan, verbose, insecure, + retry-1, + processConfig, f, + checkOutput, wantCode..., + ) + }, scan, false, verbose, insecure, c, + ) + + if f != nil { + if err := f(p, pid); err != nil { + log.Fatalf("(%s) %v", name, err) + } + } + + if scan { + testsuite.MustRun( + cred, testsuite.SessionEnv(dbusEnv), + "hakurei", "kill", p.ID.String(), + ) + testsuite.PollPID(long, pid) + } + }) +} + +func main() { + go testsuite.ReceiveSignals() + cred := syscall.Credential{Uid: 1000, Gid: 100} + + // the signal handler does not wait for termination + ctx := context.Background() + + testsuite.MustPrepareUserDirs(&cred) + var swg sync.WaitGroup + defer swg.Wait() + dbusEnv := testsuite.MustStartSessionBus(&cred) + testsuite.MustStartSway(&swg, &cred, dbusEnv) + defer testsuite.TerminateSway(&cred) + pipewireVersion := testsuite.MustStartPipeWire(&cred, dbusEnv) + sessionEnv := testsuite.SessionEnv(dbusEnv) + absNonexistent := fhs.AbsProc.Append("nonexistent") + + var wg sync.WaitGroup + defer wg.Wait() + + wg.Go(func() { + defer log.Println("validated static linking") + + for _, name := range []string{ + "hakurei", + "hsu", + "sharefs", + } { + pathname := fhs.AbsUsr.Append("local/bin", name) + testsuite.AssertOutput( + "validating "+pathname.String()+" static linking", + "", "\tnot a dynamic executable\n", + )(testsuite.MustOutput( + ctx, nil, nil, hst.ExitFailure, + "validate "+pathname.String()+" static linking", + "ldd", pathname.String(), + )) + } + }) + + wg.Go(func() { + defer log.Println("validated deny unmapped uid") + + testsuite.AssertOutput( + "denying unmapped uid", + "", "hsu: uid 65534 is not in the hsurc file\n", + )(testsuite.MustOutput( + ctx, &syscall.Credential{ + Uid: 65534, + Gid: 100, + }, nil, hst.ExitFailure, "deny unmapped uid", + "hakurei", "exec", "true", + )) + }) + + wg.Go(func() { + defer log.Println("validated deny unmapped uid (verbose)") + + testsuite.AssertOutput( + "denying unmapped uid (verbose)", + "", "hsu: uid 65534 is not in the hsurc file\n"+ + "hakurei: *cannot retrieve user id from setuid wrapper: "+ + "current user is not in the hsurc file\n", + )(testsuite.MustOutput( + ctx, &syscall.Credential{ + Uid: 65534, + Gid: 100, + }, nil, hst.ExitFailure, "deny unmapped uid (verbose)", + "hakurei", "-v", "exec", "true", + )) + }) + + wg.Go(func() { + defer log.Println("validated direct hsu call") + + testsuite.AssertOutput( + "calling hsu directly", + "0", "", + )(testsuite.MustOutput( + ctx, &cred, nil, 0, "direct hsu call", + "hsu", + )) + }) + + wg.Go(func() { + defer log.Println("validated direct hsu deny") + + testsuite.AssertOutput( + "from direct hsu denial", + "", "hsu: uid 65534 is not in the hsurc file\n", + )(testsuite.MustOutput( + ctx, &syscall.Credential{ + Uid: 65534, + Gid: 100, + }, nil, hst.ExitFailure, "direct hsu deny", + "hsu", + )) + }) + + wg.Go(func() { + defer log.Println("validated timeout behaviour") + + testsuite.AssertOutput( + "validating timeout behaviour", + "", "init: timeout exceeded waiting for lingering processes\n", + )(testsuite.MustOutput( + ctx, &cred, nil, 0, "timeout lingering process", + "hakurei", "exec", "--no-wait", + "bash", "-c", "sleep infinity & disown", + )) + }) + + wg.Go(func() { + defer log.Println("validated pd shared tmpdir") + + serial := testsuite.NewSerial() + _serial := strconv.FormatUint(serial, 10) + testsuite.AssertOutput( + "validating pd shared tmpdir", + "", "", + )(testsuite.MustOutput( + ctx, &cred, nil, 0, "pd shared tmpdir", + "hakurei", "exec", "touch", "/tmp/ok."+_serial, + )) + + if err := os.Remove( + testsuite.HakureiPrefix + "tmpdir/0/ok." + _serial, + ); err != nil { + log.Fatal(err) + } + }) + + wg.Go(func() { + defer log.Println("validated graceful failure on bad Wayland display name") + + testsuite.AssertOutput( + "validating Wayland graceful failure", + "", "hakurei: cannot connect to "+ + "/tmp/hakurei.0/compat/wayland-0: no such file or directory\n", + )(testsuite.MustOutput( + ctx, &cred, nil, hst.ExitRequest, "bad Wayland name", + "hakurei", "exec", "--wayland", + "true", + )) + }) + + wg.Go(func() { + if _, _, err := testsuite.StartHakurei( + ctx, &wg, &cred, dbusEnv, func(cmd *exec.Cmd) { + cmd.ExtraFiles = cmd.ExtraFiles[:len(cmd.ExtraFiles)-1] + }, func(err error, output string) { + defer log.Println("validated bad identifier fd") + + if err == nil { + log.Fatal("bad identity fd unexpectedly succeeded") + } + if e, ok := errors.AsType[*exec.ExitError](err); !ok { + log.Fatal(err) + } else if e.ExitCode() != hst.ExitRequest { + fmt.Print(output) + log.Fatal(e) + } + + if output != "hakurei: cannot write identifier: "+ + "bad file descriptor\r\n" { + log.Fatalf( + "bad identity fd unexpected output: %q", + output, + ) + } + }, false, false, false, false, &hst.Config{ + Container: &hst.ContainerConfig{ + Shell: absNonexistent, + Home: absNonexistent, + Path: absNonexistent, + }, + }, + ); err != nil { + log.Fatal(err) + } + }) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "indirect-termination", "indirect termination", + true, false, false, 0, nil, nil, nil, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "immediate-termination", "immediate termination", + true, false, false, 0, func(c *hst.Config) { + c.Container.WaitDelay = -1 + }, nil, nil, 254, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "shim-poke", "unexpected shim notification", + true, true, false, 0, nil, func(p hst.State, pid int) error { + if err := syscall.Kill(p.ShimPID, syscall.SIGCONT); err != nil { + return os.NewSyscallError("kill", err) + } + return nil + }, func(output string) bool { + return strings.Contains( + output, + "shim: got SIGCONT from unexpected process\r\n", + ) + }, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "reject-deprecated", "reject insecure configuration", + false, false, false, 0, func(c *hst.Config) { + c.Enablements = new(hst.EPulse) + }, nil, func(output string) bool { + return output == "hakurei: enablement PulseAudio is insecure "+ + "and no longer supported\r\n" + }, hst.ExitFailure, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "pipewire-pulse", "pipewire proxying behaviour", + false, false, false, 0, func(c *hst.Config) { + c.Enablements = new(hst.EPipeWire) + c.Container.Path = fhs.AbsUsrBin.Append("pactl") + c.Container.Args = []string{"pactl", "info"} + }, nil, func(output string) bool { + _output, _, ok := strings.CutLast(output, "Cookie: ") + if !ok { + return false + } + output, _output, ok = strings.Cut(_output, "\r\nClient Index: ") + if !ok { + return false + } + _, _output, ok = strings.Cut(_output, "\r\n") + if !ok { + return false + } + output += "\r\n" + _output + output = strings.Replace( + output, + "@DEFAULT_SINK@", + "auto_null", + 1, + ) + + return output == "Server String: unix:/run/user/65534/pulse/native\r\n"+ + "Library Protocol Version: 35\r\n"+ + "Server Protocol Version: 35\r\n"+ + "Is Local: yes\r\n"+ + "Tile Size: "+strconv.Itoa((os.Getpagesize()-4)*16)+"\r\n"+ + "User Name: nobody\r\n"+ + "Host Name: hakurei-pipewire-pulse\r\n"+ + "Server Name: PulseAudio (on PipeWire "+pipewireVersion+")\r\n"+ + "Server Version: 15.0.0\r\n"+ + "Default Sample Specification: float32le 2ch 48000Hz\r\n"+ + "Default Channel Map: front-left,front-right\r\n"+ + "Default Sink: auto_null\r\n"+ + "Default Source: @DEFAULT_SOURCE@\r\n" + }, 0, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "pipewire-security", "pipewire SecurityContext", + false, false, false, 32, func(c *hst.Config) { + c.Enablements = new(hst.EPipeWire) + c.Container.Path = fhs.AbsUsrBin.Append("pactl") + c.Container.Args = []string{ + "pactl", + "set-sink-mute", + "auto_null", + "toggle", + } + }, nil, func(output string) bool { + return output == "Failure: Access denied\r\n" + }, hst.ExitFailure, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "pipewire-direct-failure", "pipewire cover direct access", + false, false, false, 0, func(c *hst.Config) { + c.Enablements = new(hst.EPipeWire) + c.Container.Path = fhs.AbsUsrBin.Append("pw-dump") + c.Container.Args = []string{"pw-dump"} + }, nil, func(output string) bool { + return output == "can't connect: Host is down\r\n" + }, 255, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "pipewire-direct", "pipewire direct access", + false, false, true, 0, func(c *hst.Config) { + c.Enablements = new(hst.EPipeWire) + c.DirectPipeWire = true + c.Container.Path = fhs.AbsUsrBin.Append("pw-dump") + c.Container.Args = []string{"pw-dump", "-RN"} + }, nil, func(output string) bool { + return json.Valid(unsafe.Slice(unsafe.StringData(output), len(output))) + }, 0, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "wayland-client", "wayland client", + true, false, false, 0, func(c *hst.Config) { + c.Enablements = new(hst.EWayland) + c.Container.Path = fhs.AbsUsrBin.Append("foot") + c.Container.Args = []string{"foot"} + }, func(p hst.State, pid int) error { + testsuite.PollSwayWindow(ctx, &cred, func(n *testsuite.SwayNode) bool { + return n.Sandbox == "app.hakurei" && + n.ID == "app.hakurei.wayland-client" && + n.Instance == p.ID && + n.AppID == "foot" + }) + return nil + }, func(string) bool { return true }, 230, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "wayland-security-context", "wayland security-context-v1", + false, false, false, 0, func(c *hst.Config) { + c.Enablements = new(hst.EWayland) + c.Container.Path = fhs.AbsUsrBin.Append("grim") + c.Container.Args = []string{"grim"} + }, nil, func(output string) bool { + return output == "warning: zxdg_output_manager_v1 isn't available, "+ + "guessing the output layout\r\n"+ + "compositor doesn't support wlr-screencopy-unstable-v1\r\n" + }, hst.ExitFailure, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "wlr-screencopy-unstable-v1", "direct wlr-screencopy-unstable-v1", + false, false, true, 0, func(c *hst.Config) { + c.Enablements = new(hst.EWayland) + c.DirectWayland = true + c.Container.Path = fhs.AbsUsrBin.Append("grim") + c.Container.Args = []string{"grim", "/dev/null"} + }, nil, nil, 0, + ) + + // this client lacks attached identification + wg.Wait() + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "direct-wayland-client", "direct wayland client", + true, false, true, 0, func(c *hst.Config) { + c.Enablements = new(hst.EWayland) + c.DirectWayland = true + c.Container.Path = fhs.AbsUsrBin.Append("foot") + c.Container.Args = []string{"foot"} + }, func(p hst.State, pid int) error { + testsuite.PollSwayWindow(ctx, &cred, func(n *testsuite.SwayNode) bool { + return n.Sandbox == "" && + n.ID == "" && + n.Instance == hst.ID{} && + n.AppID == "foot" + }) + return nil + }, func(string) bool { return true }, 230, + ) + wg.Wait() + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "xwayland-client", "xwayland client", + false, false, false, 32, func(c *hst.Config) { + c.Enablements = new(hst.EX11) + c.Container.Path = fhs.AbsUsrBin.Append("xset") + c.Container.Args = []string{"xset", "q"} + c.Container.Flags |= hst.FHostAbstract + }, nil, func(output string) bool { + return strings.Contains(output, "DPMS") + }, 0, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "dbus", "proxied dbus call", + false, false, false, 0, func(c *hst.Config) { + c.Enablements = new(hst.EDBus) + c.SessionBus = &hst.BusConfig{ + Talk: []string{"org.freedesktop.Notifications"}, + Filter: true, + } + c.Container.Path = fhs.AbsUsrBin.Append("notify-send") + c.Container.Args = []string{ + "notify-send", + "Test notification", + } + }, nil, func(output string) bool { + return output == "GDBus.Error:"+ + "org.freedesktop.DBus.Error.ServiceUnknown: "+ + "The name org.freedesktop.Notifications "+ + "was not provided by any .service files\r\n" + }, hst.ExitFailure, + ) + + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "dbus reject", "rejected dbus call", + false, false, false, 0, func(c *hst.Config) { + c.Enablements = new(hst.EDBus) + c.SessionBus = &hst.BusConfig{Filter: true} + c.Container.Path = fhs.AbsUsrBin.Append("notify-send") + c.Container.Args = []string{ + "notify-send", + "Test notification", + } + }, nil, func(output string) bool { + return output == "GDBus.Error:"+ + "org.freedesktop.DBus.Error.ServiceUnknown: "+ + "org.freedesktop.DBus.Error.ServiceUnknown\r\n" + }, hst.ExitFailure, + ) + + silentOutputInterrupt := func(args ...string) { + wg.Go(func() { + defer log.Printf( + "validated silent output after interrupt (args: %q)", + strings.Join(args, " "), + ) + + serial := testsuite.NewSerial() + _serial := strconv.FormatUint(serial, 10) + cmd := exec.CommandContext(ctx, "hakurei", slices.Concat( + []string{"exec"}, + args, + []string{"sh", "-c", + "touch /tmp/ok." + _serial + " && sleep infinity"}, + )...) + var stdout, stderr strings.Builder + cmd.Stdout, cmd.Stderr = &stdout, &stderr + cmd.SysProcAttr = &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGTERM, + Credential: &cred, + Setpgid: true, + } + testsuite.MustAppendEnv(cmd, sessionEnv...) + + done := testsuite.MustStart(cmd) + for { + select { + case err := <-done: + fmt.Print(stdout.String()) + fmt.Print(stderr.String()) + if err == nil { + log.Fatalf( + "test process %d terminated unexpectedly", + serial, + ) + } + log.Fatalf( + "test process %d terminated unexpectedly: %v", + serial, err, + ) + default: + break + } + + time.Sleep(100 * time.Millisecond) + if err := os.Remove( + testsuite.HakureiPrefix + "tmpdir/0/ok." + _serial, + ); err != nil { + if errors.Is(err, os.ErrNotExist) { + continue + } + log.Fatal(err) + } + break + } + + if err := syscall.Kill( + -cmd.Process.Pid, + syscall.SIGINT, + ); err != nil { + log.Fatalf( + "cannot interrupt process group %d: %v", + serial, err, + ) + } + + if err := <-done; err == nil { + log.Fatal("interrupted process unexpectedly succeeded") + } else if e, ok := errors.AsType[*exec.ExitError](err); !ok { + log.Fatalf("validate silent interrupt: %v", err) + } else if code := e.ExitCode(); !slices.Contains([]int{ + hst.ExitFailure, + hst.ExitCancel, + 127, 130, 254, + }, code) { + log.Fatalf("validate silent interrupt: %v", e) + } + + if stdout.Len() != 0 || stderr.Len() != 0 { + log.Fatalf( + "interrupt is not silent (args: %q)"+ + "\nstdout = %q\nstderr = %q", + strings.Join(args, " "), + stdout.String(), + stderr.String(), + ) + } + }) + } + silentOutputInterrupt() + silentOutputInterrupt("--dbus") + silentOutputInterrupt("--wayland", "-X", "--dbus", "--pulse") + + const RLIMIT_RTPRIO = 14 + if err := syscall.Setrlimit(RLIMIT_RTPRIO, &syscall.Rlimit{ + Cur: 49, + Max: 49, + }); err != nil { + log.Fatalf("cannot increase rtprio limit: %v", err) + } + for _, policy := range []ext.SchedPolicy{ + ext.SCHED_NORMAL, + ext.SCHED_IDLE, + ext.SCHED_RR, + } { + _policy := policy + checkHakurei( + ctx, &wg, &cred, dbusEnv, + "setscheduler-"+strconv.Itoa(int(policy)), "setscheduler "+strconv.Quote(policy.String()), + true, false, false, 0, func(c *hst.Config) { + c.SchedPolicy = _policy + }, func(p hst.State, pid int) error { + got := -1 + r, err := os.Open(filepath.Join( + fhs.Proc, + strconv.Itoa(pid), + "sched", + )) + if err != nil { + return err + } + + s := bufio.NewScanner(r) + + for range 2 { + if !s.Scan() { + _ = r.Close() + if err = s.Err(); err != nil { + return err + } + return errors.New("sched header too short") + } + } + + for s.Scan() { + if !strings.HasPrefix(s.Text(), "policy") { + continue + } + + k, v, ok := strings.Cut(s.Text(), ":") + if !ok { + continue + } + if strings.TrimSpace(k) != "policy" { + continue + } + + got, err = strconv.Atoi(strings.TrimSpace(v)) + if err != nil { + _ = r.Close() + return err + } + } + if err = s.Err(); err != nil { + _ = r.Close() + return err + } else if err = r.Close(); err != nil { + return err + } + + if _got := ext.SchedPolicy(got); _got != policy { + return fmt.Errorf("unexpected %q policy %q", policy, _got) + } + return nil + }, nil, + ) + } + + wg.Go(func() { + defer log.Println("validated revert type selection") + + c := newConfig("rts", "sleep", "infinity") + c.Container.Flags |= hst.FShareRuntime + uid := hst.ToUser(0, c.Identity) + + onDone := func(err error, output string) { + if err == nil { + log.Fatal("revert type selection test process " + + "terminated unexpectedly") + } else if e, ok := errors.AsType[*exec.ExitError](err); !ok { + log.Fatalf("revert type selection: %v\n%s", err, output) + } else if e.ExitCode() != 130 { + log.Fatalf("revert type selection: %v\n%s", e, output) + } + + if output != "" { + log.Fatalf("unexpected output\n%s", output) + } + } + + validatePresent := func(step string) { + testsuite.AssertOutput( + "validating ACLs for rts present "+step, + "user::rwx\n"+ + "user:"+strconv.Itoa(uid)+":rwx\n"+ + "group::---\n"+ + "mask::rwx\n"+ + "other::---\n"+ + "\n", + "", + )(testsuite.MustOutput( + ctx, &cred, nil, 0, "getfacl rts present", + "getfacl", + "--absolute-names", + "--omit-header", + "--numeric", + "/tmp/hakurei.0/runtime/"+strconv.Itoa(c.Identity), + )) + } + + p0, pid0 := testsuite.MustStartHakurei( + ctx, &wg, &cred, dbusEnv, func(err error, output string) { + onDone(err, output) + testsuite.AssertOutput( + "validating ACLs for rts reverted", + "user::rwx\n"+ + "group::---\n"+ + "mask::---\n"+ + "other::---\n"+ + "\n", + "", + )(testsuite.MustOutput( + ctx, &cred, nil, 0, "getfacl rts present", + "getfacl", + "--absolute-names", + "--omit-header", + "--numeric", + "/tmp/hakurei.0/runtime/"+strconv.Itoa(c.Identity), + )) + }, true, false, false, false, c, + ) + validatePresent("after first process") + + done := make(chan struct{}) + p1, pid1 := testsuite.MustStartHakurei( + ctx, &wg, &cred, dbusEnv, func(err error, output string) { + defer close(done) + onDone(err, output) + validatePresent("after second process termination") + }, true, false, false, false, c, + ) + validatePresent("after second process") + + testsuite.MustRun( + &cred, testsuite.SessionEnv(dbusEnv), + "hakurei", "kill", p1.ID.String(), + ) + testsuite.PollPID("revert type selection 1", pid1) + + <-done + testsuite.MustRun( + &cred, testsuite.SessionEnv(dbusEnv), + "hakurei", "kill", p0.ID.String(), + ) + testsuite.PollPID("revert type selection 0", pid0) + }) + + wg.Wait() + + if dents, err := os.ReadDir(fhs.Tmp); err != nil { + log.Fatal(err) + } else { + for _, dent := range dents { + if name := dent.Name(); strings.HasPrefix(name, ".hakurei-shim-") { + log.Fatalf("leftover shim work dir %q", name) + } + } + } + + if dents, err := os.ReadDir(testsuite.HakureiPrefix); err != nil { + log.Fatal(err) + } else { + if len(dents) != 4 { + log.Fatal("leftover hakurei work directories") + } + } + + if err := filepath.WalkDir(testsuite.HakureiPrefix+"state", func( + path string, + d fs.DirEntry, + err error, + ) error { + if err != nil { + return err + } + + if d.IsDir() { + return nil + } + + if !d.Type().IsRegular() { + return fmt.Errorf("strange entry at %q: %s", path, d.Type()) + } + if d.Name() != "lock" { + return fmt.Errorf("leftover entry at %q", path) + } + return nil + }); err != nil { + log.Fatal(err) + } + + var identifiers []hst.ID + testsuite.MustDecodeFrom( + ctx, &cred, nil, &identifiers, + "hakurei", "--json", "ps", "--short", + ) + if len(identifiers) != 0 { + log.Fatalf("%d instances still running", len(identifiers)) + } +} diff --git a/cmd/hakurei/testsuite/sandbox/main.go b/cmd/hakurei/testsuite/sandbox/main.go new file mode 100644 index 00000000..681718a7 --- /dev/null +++ b/cmd/hakurei/testsuite/sandbox/main.go @@ -0,0 +1,390 @@ +//go:build testsuite + +// The sandbox test program runs cmd/hakurei with configurations simulating +// several common workloads and inspects the resulting container states. +package main + +import ( + "context" + "errors" + "fmt" + "log" + "os" + "os/exec" + "os/signal" + "path/filepath" + "slices" + "strconv" + "strings" + "sync" + "syscall" + + "hakurei.app/fhs" + "hakurei.app/hst" + "hakurei.app/internal/testsuite" + + "hakurei.app/cmd/hakurei/testsuite/sandbox/testdata" +) + +// mustStart starts a hakurei container and returns the pid of a process within +// the container. This process must be terminated by the caller. +func mustStart( + ctx context.Context, + serial uint64, identity int, + cred *syscall.Credential, + files ...*os.File, +) (pid int, done <-chan error) { + _serial := strconv.FormatUint(serial, 10) + _, done = testsuite.MustStartWith( + ctx, cred, false, nil, files, + "hakurei", "exec", + "-a", strconv.Itoa(identity), + "sleep", "infinity", _serial, + ) + + var stat syscall.Stat_t + pid = testsuite.MustScanFor(func(s *testsuite.StatScanner) bool { + select { + case err := <-done: + if err == nil { + log.Fatalf("test process %d terminated unexpectedly", serial) + } + log.Fatalf("test process %d terminated unexpectedly: %v", serial, err) + default: + break + } + + if s.Stat().Comm != "sleep" { + return false + } + + if args, err := s.Stat().Args(); err != nil { + if testsuite.IsNotExist(err) { + return false + } + log.Fatal(err) + } else if !slices.Equal(args, []string{ + "sleep", + "infinity", + _serial, + }) { + return false + } + + if err := s.Stat().Stat(&stat); err != nil { + if testsuite.IsNotExist(err) { + return false + } + log.Fatal(err) + } + + id := hst.ToUser[uint32](0, uint32(identity)) + if stat.Uid != id || stat.Gid != id { + return false + } + + return true + }) + return +} + +// exercise exercises internal/outcome container setup and teardown. +func exercise( + ctx context.Context, + cred *syscall.Credential, + n, count int, + f func(identity, pid int), +) { + + var wg sync.WaitGroup + in := count + var identity int + + wg.Add(n) + for range n { + in-- + if in < 0 { + identity++ + in = count + } + go func(identity int) { + defer wg.Done() + + for ctx.Err() == nil { + pid, done := mustStart( + ctx, + testsuite.NewSerial(), + identity, + cred, + ) + + if _err := syscall.Kill(pid, syscall.SIGTERM); _err != nil { + log.Fatal(_err) + } + if _err := testsuite.FilterTerminated(<-done); _err != nil { + log.Fatal(_err) + } + if f != nil { + f(identity, pid) + } + } + }(identity) + } + wg.Wait() +} + +func main() { + cred := syscall.Credential{Uid: 1000, Gid: 100} + + if p, ok := os.LookupEnv("HAKUREI_TESTSUITE_EXERCISE"); ok { + n, err := strconv.Atoi(p) + if err != nil { + log.Fatalf("invalid stress concurrency %q", p) + } + + count := n + if p, ok = os.LookupEnv("HAKUREI_TESTSUITE_EXERCISE_COUNT"); ok { + count, err = strconv.Atoi(p) + if err != nil { + log.Fatalf("invalid stress instance count %q", p) + } + } + + ctx, stop := signal.NotifyContext(context.Background(), + os.Interrupt, + syscall.SIGTERM, + ) + defer stop() + log.Printf("exercising cmd/hakurei with %d concurrent instances", n) + var mu sync.Mutex + exercise(ctx, &cred, n, count, func(identity, pid int) { + mu.Lock() + fmt.Print(strconv.Itoa(identity) + "\t" + strconv.Itoa(pid) + "\r") + mu.Unlock() + }) + } + + go testsuite.ReceiveSignals() + + // the signal handler does not wait for termination + ctx := context.Background() + + if err := os.MkdirAll("/opt/test-helper/bin", 0755); err != nil { + log.Fatal(err) + } + + var testHelperDone <-chan error + { + cmd := exec.Command( + "go", "build", + "-o", "/opt/test-helper/bin", + "-tags=tester", + "-trimpath", + "./cmd/hakurei/testsuite/sandbox/tester", + ) + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + testHelperDone = testsuite.MustStart(cmd) + } + + var wg sync.WaitGroup + defer wg.Wait() + + wg.Go(func() { + defer log.Println("validated capabilities/securebits in user namespace") + + testsuite.AssertOutput( + "validating prevailing capability and related state", + "Current: =\n"+ + "Bounding set =\n"+ + "Ambient set =\n"+ + "Current IAB: !cap_chown,!cap_dac_override,"+ + "!cap_dac_read_search,!cap_fowner,!cap_fsetid,!cap_kill,"+ + "!cap_setgid,!cap_setuid,!cap_setpcap,!cap_linux_immutable,"+ + "!cap_net_bind_service,!cap_net_broadcast,!cap_net_admin,"+ + "!cap_net_raw,!cap_ipc_lock,!cap_ipc_owner,!cap_sys_module,"+ + "!cap_sys_rawio,!cap_sys_chroot,!cap_sys_ptrace,"+ + "!cap_sys_pacct,!cap_sys_admin,!cap_sys_boot,!cap_sys_nice,"+ + "!cap_sys_resource,!cap_sys_time,!cap_sys_tty_config,"+ + "!cap_mknod,!cap_lease,!cap_audit_write,!cap_audit_control,"+ + "!cap_setfcap,!cap_mac_override,!cap_mac_admin,!cap_syslog,"+ + "!cap_wake_alarm,!cap_block_suspend,!cap_audit_read,"+ + "!cap_perfmon,!cap_bpf,!cap_checkpoint_restore\n"+ + "Securebits: 00/0x0/1'b0 (no-new-privs=1)\n"+ + " secure-noroot: no (unlocked)\n"+ + " secure-no-suid-fixup: no (unlocked)\n"+ + " secure-keep-caps: no (unlocked)\n"+ + " secure-no-ambient-raise: no (unlocked)\n"+ + "uid=65534(chronos) euid=65534(chronos)\n"+ + "gid=65534(hakurei)\n"+ + "groups=65534(hakurei)\n"+ + "Guessed mode: HYBRID (4)\n", "", + )(testsuite.MustOutput( + ctx, &cred, nil, 0, "capsh", + "hakurei", "exec", "capsh", "--print", + )) + + testsuite.AssertOutput( + "validating host no-new-privs", + "", "no-new-privs not set\n", + )(testsuite.MustOutput( + ctx, &cred, nil, 1, "host no-new-privs", + "capsh", "--has-no-new-privs", + )) + + testsuite.AssertOutput( + "validating no-new-privs", + "", "", + )(testsuite.MustOutput( + ctx, &cred, nil, 0, "no-new-privs", + "hakurei", "exec", "capsh", "--has-no-new-privs", + )) + + for _, p := range [][2]string{ + {"ambient", "in ambient vector"}, + {"bounding", "in bounding vector"}, + {"inheritable"}, + {"permitted"}, + } { + if p[1] == "" { + p[1] = p[0] + } + testsuite.AssertOutput( + "checking CAP_SYS_ADMIN not "+p[1], + "", "cap[CAP_SYS_ADMIN] not "+p[1]+"\n", + )(testsuite.MustOutput( + ctx, &cred, nil, 1, p[0], + "hakurei", "exec", + "capsh", "--has-"+p[0][:1]+"=CAP_SYS_ADMIN", + )) + } + + testsuite.AssertOutput( + "attempting unmount", + "", "umount: /tmp: must be superuser to unmount.\n", + )(testsuite.MustOutput( + ctx, &cred, nil, 32, "attempt unmount", + "hakurei", "exec", "umount", "/tmp", + )) + }) + + wg.Go(func() { + defer log.Println("validated pd seccomp outcome") + + c, cancel := context.WithCancel(ctx) + defer cancel() + + pid, done := mustStart(c, testsuite.NewSerial(), 0, &cred) + testsuite.MustCheckFilter("pd", pid, testdata.SumPD) + if err := testsuite.FilterTerminated(<-done); err != nil { + log.Fatal(err) + } + }) + + wg.Go(func() { + defer log.Println("validated fd leak") + + c, cancel := context.WithCancel(ctx) + defer cancel() + + pid, done := mustStart(c, testsuite.NewSerial(), 0, &cred, os.Stdin, os.Stdout, os.Stderr) + prefix := filepath.Join(fhs.Proc, strconv.Itoa(pid), "fd") + + retry: // TODO(ophestra): use statically linked anchor program + fail := false + if entries, err := os.ReadDir(prefix); err != nil { + log.Fatal(err.Error()) + } else { + for _, ent := range entries { + var fd int + if fd, err = strconv.Atoi(ent.Name()); err != nil { + log.Fatal(err.Error()) + } + + // skip standard streams + if fd <= 2 { + continue + } + fail = true + + var d string + if d, err = os.Readlink(filepath.Join( + prefix, + ent.Name(), + )); err != nil { + if errors.Is(err, os.ErrNotExist) { + goto retry + } + log.Fatal(err.Error()) + } + if strings.HasPrefix(d, "/lib") { + goto retry + } + log.Printf("extra fd %d -> %s", fd, d) + } + } + if fail { + log.Fatal("file descriptors leaked") + } + + if err := syscall.Kill(pid, syscall.SIGTERM); err != nil { + log.Fatalf("cannot terminate anchor: %v", err) + } else if err = testsuite.FilterTerminated(<-done); err != nil { + log.Fatal(err) + } + }) + + testsuite.MustPrepareUserDirs(&cred) + var swg sync.WaitGroup + defer swg.Wait() + dbusEnv := testsuite.MustStartSessionBus(&cred) + testsuite.MustStartSway(&swg, &cred, dbusEnv) + defer testsuite.TerminateSway(&cred) + testsuite.MustStartPipeWire(&cred, dbusEnv) + + if err := <-testHelperDone; err != nil { + log.Fatalf("cannot compile test helper: %v", err) + } + log.Println("created test helper") + + for name, tc := range testdata.All() { + wg.Go(func() { + c, cancel := context.WithCancel(ctx) + + _, pid, err := testsuite.StartHakurei( + c, &wg, &cred, dbusEnv, nil, + func(err error, output string) { + defer cancel() + + log.Printf("completed test case %s", name) + if err != nil { + log.Fatalf("test case %s: %v\n%s", name, err, output) + } + }, true, true, false, false, &tc.Hakurei, + ) + + if err != nil { + log.Printf( + "cannot start test case %s: %v", + name, + err, + ) + cancel() + // onDone eventually terminates the test suite + } else { + testsuite.MustCheckFilter(name, pid, tc.Sum) + } + }) + } + + wg.Wait() + + if dents, err := os.ReadDir("/tmp"); err != nil { + log.Fatal(err) + } else { + for _, dent := range dents { + if name := dent.Name(); strings.HasPrefix(name, ".hakurei-shim-") { + log.Fatalf("leftover shim work dir %q", name) + } + } + } +} diff --git a/cmd/hakurei/testsuite/sandbox/testdata/device.go b/cmd/hakurei/testsuite/sandbox/testdata/device.go new file mode 100644 index 00000000..5de9f550 --- /dev/null +++ b/cmd/hakurei/testsuite/sandbox/testdata/device.go @@ -0,0 +1,134 @@ +//go:build testsuite || tester + +package testdata + +import ( + "os" + "syscall" + + "hakurei.app/fhs" + "hakurei.app/hst" + "hakurei.app/internal/testsuite" + "hakurei.app/internal/testsuite/mountinfo" +) + +var _ = TestCase{ + Hakurei: hst.Config{ + ID: "app.hakurei.sample.device", + Enablements: new(hst.EWayland | hst.EPipeWire | hst.EDBus | hst.EX11), + Identity: 4, + + Container: &hst.ContainerConfig{ + Hostname: "hakurei-sample-device", + + Filesystem: []hst.FilesystemConfigJSON{ + fcLinker, + fcLib, + fcTestHelper, + }, + + Username: "u0_a4", + Shell: fhs.AbsUsrBin.Append("bash"), + Home: hst.AbsPrivateTmp, + Path: absTestHelper, + Args: []string{"tester", "device"}, + + Flags: hst.FDevice | hst.FShareTmpdir, + }, + }, + + // 0, PresetStrict + Sum: sumSimple, + + Env: []string{ + "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus", + "DISPLAY=unix:/tmp/.X11-unix/X0", + "HOME=/.hakurei", + "SHELL=/usr/bin/bash", + "TERM=xterm", + "USER=u0_a4", + "WAYLAND_DISPLAY=wayland-0", + "XDG_RUNTIME_DIR=/run/user/65534", + "XDG_SESSION_CLASS=user", + "XDG_SESSION_TYPE=wayland", + "PULSE_SERVER=unix:/run/user/65534/pulse/native", + }, + + FS: &testsuite.FS{Dir: dir{ + ".hakurei": {Mode: os.ModeDir | 0755, Dir: dir{ + "test-helper": {Mode: 0755}, + }}, + + // unstable host dev + "dev": {Mode: os.ModeDir | 0755}, + + "etc": {Mode: os.ModeDir | 0755, Dir: dir{ + "passwd": {Mode: 0600, + Data: new("u0_a4:x:65534:65534:Hakurei:/.hakurei:/usr/bin/bash\n")}, + "group": {Mode: 0600, + Data: new("hakurei:x:65534:\n")}, + }}, + + "lib64": {Mode: os.ModeDir | 0755, Dir: dir{ + "ld-linux-x86-64.so.2": {Mode: os.ModeSymlink | 0777}, + }}, + + "run": {Mode: os.ModeDir | 0755, Dir: dir{ + "user": {Mode: os.ModeDir | 0755, Dir: dir{ + "65534": {Mode: os.ModeDir | 0700, Dir: dir{ + "bus": {Mode: os.ModeSocket | 0775}, + "wayland-0": {Mode: os.ModeSocket | 070}, + "pulse": {Mode: os.ModeDir | 0700, Dir: dir{ + "native": {Mode: os.ModeSocket | 0777}, + }}, + }}, + }}, + }}, + + "tmp": {Mode: os.ModeDir | 0770, Dir: dir{ + ".X11-unix": {Mode: os.ModeDir | 0755, Dir: dir{ + "X0": {Mode: os.ModeSocket | 0775}, + }}, + }}, + + "lib": {Mode: os.ModeDir | 0755}, + "proc": {Mode: os.ModeDir | 0555}, + }}, + + Mount: []*mountinfo.Entry{ + r("/sysroot", "/", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110003,gid=110003,inode64"), + r("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"), + r("/", "/.hakurei", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,size=4k,mode=755,uid=110003,gid=110003,inode64"), + + // host /dev in testing environment + r("/", "/dev", "rw,nosuid", "tmpfs", "tmpfs", ignore), + r("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,gid=100004,mode=620,ptmxmode=666"), + r("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"), + r("/kvm", "/dev/kvm", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/fuse", "/dev/fuse", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/", "/dev/shm", "rw,nosuid,nodev,noexec,relatime", "tmpfs", "shm", ignore), + r("/null", "/dev/null", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/zero", "/dev/zero", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/full", "/dev/full", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/tty", "/dev/tty", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/random", "/dev/random", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/urandom", "/dev/urandom", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + + r("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,uid=110003,gid=110003,inode64"), + r("/", "/run/user", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,size=16384k,mode=755,uid=110003,gid=110003,inode64"), + r("/tmp/hakurei.0/tmpdir/4", "/tmp", "rw,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r(ignore, "/etc/passwd", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110003,gid=110003,inode64"), + r(ignore, "/etc/group", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110003,gid=110003,inode64"), + r(ignore, "/run/user/65534/wayland-0", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r("/tmp/.X11-unix", "/tmp/.X11-unix", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r(ignore, "/run/user/65534/bus", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r("/usr/lib", "/lib", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r("/opt/test-helper/bin/tester", "/.hakurei/test-helper", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r(ignore, "/run/user/65534/pulse/native", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + }, + + Seccomp: true, + + TrySocket: "/tmp/.X11-unix/X0", + ErrnoAbstract: syscall.ECONNREFUSED, +}.register("device") diff --git a/cmd/hakurei/testsuite/sandbox/testdata/mapuid.go b/cmd/hakurei/testsuite/sandbox/testdata/mapuid.go new file mode 100644 index 00000000..218b035d --- /dev/null +++ b/cmd/hakurei/testsuite/sandbox/testdata/mapuid.go @@ -0,0 +1,124 @@ +//go:build testsuite || tester + +package testdata + +import ( + "os" + "syscall" + + "hakurei.app/fhs" + "hakurei.app/hst" + "hakurei.app/internal/testsuite" + "hakurei.app/internal/testsuite/mountinfo" +) + +var _ = TestCase{ + Hakurei: hst.Config{ + ID: "app.hakurei.sample.mapuid", + Enablements: new(hst.EWayland | hst.EPipeWire | hst.EDBus), + Identity: 3, + + Container: &hst.ContainerConfig{ + Hostname: "hakurei-sample-mapuid", + + Filesystem: []hst.FilesystemConfigJSON{ + fcLinker, + fcLib, + fcTestHelper, + }, + + Username: "u0_a3", + Shell: fhs.AbsUsrBin.Append("bash"), + Home: hst.AbsPrivateTmp, + Path: absTestHelper, + Args: []string{"tester", "mapuid"}, + + Flags: hst.FMapRealUID | hst.FShareRuntime | hst.FShareTmpdir, + }, + }, + + // 0, PresetStrict + Sum: sumSimple, + + Env: []string{ + "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus", + "HOME=/.hakurei", + "SHELL=/usr/bin/bash", + "TERM=xterm", + "USER=u0_a3", + "WAYLAND_DISPLAY=wayland-0", + "XDG_RUNTIME_DIR=/run/user/1000", + "XDG_SESSION_CLASS=user", + "XDG_SESSION_TYPE=wayland", + "PULSE_SERVER=unix:/run/user/1000/pulse/native", + }, + + FS: &testsuite.FS{Dir: dir{ + ".hakurei": {Mode: os.ModeDir | 0755, Dir: dir{ + "test-helper": {Mode: 0755}, + }}, + + // unstable host dev + "dev": {Mode: os.ModeDir | 0755}, + + "etc": {Mode: os.ModeDir | 0755, Dir: dir{ + "passwd": {Mode: 0600, + Data: new("u0_a3:x:1000:100:Hakurei:/.hakurei:/usr/bin/bash\n")}, + "group": {Mode: 0600, + Data: new("hakurei:x:100:\n")}, + }}, + + "lib64": {Mode: os.ModeDir | 0755, Dir: dir{ + "ld-linux-x86-64.so.2": {Mode: os.ModeSymlink | 0777}, + }}, + + "run": {Mode: os.ModeDir | 0755, Dir: dir{ + "user": {Mode: os.ModeDir | 0755, Dir: dir{ + "1000": {Mode: os.ModeDir | 0770, Dir: dir{ + "bus": {Mode: os.ModeSocket | 0775}, + "wayland-0": {Mode: os.ModeSocket | 070}, + "pulse": {Mode: os.ModeDir | 0700, Dir: dir{ + "native": {Mode: os.ModeSocket | 0777}, + }}, + }}, + }}, + }}, + + "tmp": {Mode: os.ModeDir | 0770, Dir: dir{}}, + + "lib": {Mode: os.ModeDir | 0755}, + "proc": {Mode: os.ModeDir | 0555}, + }}, + + Mount: []*mountinfo.Entry{ + r("/sysroot", "/", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110002,gid=110002,inode64"), + r("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"), + r("/", "/.hakurei", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,size=4k,mode=755,uid=110002,gid=110002,inode64"), + r("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", "rw,mode=755,uid=110002,gid=110002,inode64"), + r("/null", "/dev/null", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/zero", "/dev/zero", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/full", "/dev/full", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/random", "/dev/random", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/urandom", "/dev/urandom", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/tty", "/dev/tty", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"), + r("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"), + r("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,uid=110002,gid=110002,inode64"), + r("/", "/run/user", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,size=16384k,mode=755,uid=110002,gid=110002,inode64"), + r("/tmp/hakurei.0/runtime/3", "/run/user/1000", "rw,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r("/tmp/hakurei.0/tmpdir/3", "/tmp", "rw,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r(ignore, "/etc/passwd", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110002,gid=110002,inode64"), + r(ignore, "/etc/group", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110002,gid=110002,inode64"), + r(ignore, "/run/user/1000/wayland-0", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r(ignore, "/run/user/1000/bus", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r("/usr/lib", "/lib", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r("/opt/test-helper/bin/tester", "/.hakurei/test-helper", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r(ignore, "/run/user/1000/pulse/native", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + }, + + Seccomp: true, + + TrySocket: "/tmp/.X11-unix/X0", + ErrnoAbstract: syscall.ECONNREFUSED, + ErrnoPathname: syscall.ENOENT, +}.register("mapuid") diff --git a/cmd/hakurei/testsuite/sandbox/testdata/pdlike.go b/cmd/hakurei/testsuite/sandbox/testdata/pdlike.go new file mode 100644 index 00000000..5b58ed38 --- /dev/null +++ b/cmd/hakurei/testsuite/sandbox/testdata/pdlike.go @@ -0,0 +1,141 @@ +//go:build testsuite || tester + +package testdata + +import ( + "os" + "syscall" + + "hakurei.app/fhs" + "hakurei.app/hst" + "hakurei.app/internal/testsuite" + "hakurei.app/internal/testsuite/mountinfo" +) + +var _ = TestCase{ + Hakurei: hst.Config{ + ID: "app.hakurei.sample.pdlike", + Enablements: new(hst.EWayland | hst.EPipeWire | hst.EDBus), + Identity: 5, + + Container: &hst.ContainerConfig{ + Hostname: "hakurei-sample-pdlike", + + Filesystem: []hst.FilesystemConfigJSON{ + fcLinker, + fcLib, + fcTestHelper, + }, + + Username: "u0_a5", + Shell: fhs.AbsUsrBin.Append("bash"), + Home: hst.AbsPrivateTmp, + Path: absTestHelper, + Args: []string{"tester", "pdlike"}, + + Flags: hst.FHostNet | hst.FTty | hst.FUserns | hst.FShareRuntime | hst.FShareTmpdir, + }, + }, + + // 0, PresetExt | PresetDenyDevel + Sum: SumPD, + + Env: []string{ + "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus", + "HOME=/.hakurei", + "SHELL=/usr/bin/bash", + "TERM=xterm", + "USER=u0_a5", + "WAYLAND_DISPLAY=wayland-0", + "XDG_RUNTIME_DIR=/run/user/65534", + "XDG_SESSION_CLASS=user", + "XDG_SESSION_TYPE=wayland", + "PULSE_SERVER=unix:/run/user/65534/pulse/native", + }, + + FS: &testsuite.FS{Dir: dir{ + ".hakurei": {Mode: os.ModeDir | 0755, Dir: dir{ + "test-helper": {Mode: 0755}, + }}, + + "dev": {Mode: os.ModeDir | 0755, Dir: dir{ + "core": {Mode: os.ModeSymlink | 0777}, + "fd": {Mode: os.ModeSymlink | 0777}, + "full": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, + "mqueue": {Mode: os.ModeDir | os.ModeSticky | 0777, Dir: dir{}}, + "null": {Mode: os.ModeDevice | os.ModeCharDevice | 0666, Data: new("")}, + "ptmx": {Mode: os.ModeSymlink | 0777}, + "pts": {Mode: os.ModeDir | 0755, Dir: dir{ + "ptmx": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, + }}, + "random": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, + "shm": {Mode: os.ModeDir | os.ModeSticky | 0777, Dir: dir{}}, + "stderr": {Mode: os.ModeSymlink | 0777}, + "stdin": {Mode: os.ModeSymlink | 0777}, + "stdout": {Mode: os.ModeSymlink | 0777}, + "tty": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, + "urandom": {Mode: os.ModeDevice | os.ModeCharDevice | 0444}, + "zero": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, + }}, + + "etc": {Mode: os.ModeDir | 0755, Dir: dir{ + "passwd": {Mode: 0600, + Data: new("u0_a5:x:65534:65534:Hakurei:/.hakurei:/usr/bin/bash\n")}, + "group": {Mode: 0600, + Data: new("hakurei:x:65534:\n")}, + }}, + + "lib64": {Mode: os.ModeDir | 0755, Dir: dir{ + "ld-linux-x86-64.so.2": {Mode: os.ModeSymlink | 0777}, + }}, + + "run": {Mode: os.ModeDir | 0755, Dir: dir{ + "user": {Mode: os.ModeDir | 0755, Dir: dir{ + "65534": {Mode: os.ModeDir | 0770, Dir: dir{ + "bus": {Mode: os.ModeSocket | 0775}, + "wayland-0": {Mode: os.ModeSocket | 070}, + "pulse": {Mode: os.ModeDir | 0700, Dir: dir{ + "native": {Mode: os.ModeSocket | 0777}, + }}, + }}, + }}, + }}, + + "tmp": {Mode: os.ModeDir | 0770, Dir: dir{}}, + + "lib": {Mode: os.ModeDir | 0755}, + "proc": {Mode: os.ModeDir | 0555}, + }}, + + Mount: []*mountinfo.Entry{ + r("/sysroot", "/", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110004,gid=110004,inode64"), + r("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"), + r("/", "/.hakurei", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,size=4k,mode=755,uid=110004,gid=110004,inode64"), + r("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", "rw,mode=755,uid=110004,gid=110004,inode64"), + r("/null", "/dev/null", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/zero", "/dev/zero", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/full", "/dev/full", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/random", "/dev/random", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/urandom", "/dev/urandom", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/tty", "/dev/tty", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"), + r("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"), + r("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,uid=110004,gid=110004,inode64"), + r("/", "/run/user", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,size=16384k,mode=755,uid=110004,gid=110004,inode64"), + r("/tmp/hakurei.0/runtime/5", "/run/user/65534", "rw,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r("/tmp/hakurei.0/tmpdir/5", "/tmp", "rw,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r(ignore, "/etc/passwd", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110004,gid=110004,inode64"), + r(ignore, "/etc/group", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110004,gid=110004,inode64"), + r(ignore, "/run/user/65534/wayland-0", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r(ignore, "/run/user/65534/bus", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r("/usr/lib", "/lib", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r("/opt/test-helper/bin/tester", "/.hakurei/test-helper", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r(ignore, "/run/user/65534/pulse/native", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + }, + + Seccomp: true, + + TrySocket: "/tmp/.X11-unix/X0", + ErrnoAbstract: syscall.EPERM, + ErrnoPathname: syscall.ENOENT, +}.register("pdlike") diff --git a/cmd/hakurei/testsuite/sandbox/testdata/simple.go b/cmd/hakurei/testsuite/sandbox/testdata/simple.go new file mode 100644 index 00000000..edb7c350 --- /dev/null +++ b/cmd/hakurei/testsuite/sandbox/testdata/simple.go @@ -0,0 +1,140 @@ +//go:build testsuite || tester + +package testdata + +import ( + "os" + "syscall" + + "hakurei.app/fhs" + "hakurei.app/hst" + "hakurei.app/internal/testsuite" + "hakurei.app/internal/testsuite/mountinfo" +) + +var _ = TestCase{ + Hakurei: hst.Config{ + ID: "app.hakurei.sample.simple", + Enablements: new(hst.EWayland | hst.EPipeWire | hst.EDBus), + Identity: 1, + + Container: &hst.ContainerConfig{ + Hostname: "hakurei-sample-simple", + Env: map[string]string{"HAKUREI_SAMPLE": "1"}, + + Filesystem: []hst.FilesystemConfigJSON{ + fcLinker, + fcLib, + fcTestHelper, + }, + + Username: "u0_a1", + Shell: fhs.AbsUsrBin.Append("bash"), + Home: hst.AbsPrivateTmp, + Path: absTestHelper, + Args: []string{"tester", "simple"}, + }, + }, + + // 0, PresetStrict + Sum: sumSimple, + + Env: []string{ + "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus", + "HAKUREI_SAMPLE=1", + "HOME=/.hakurei", + "SHELL=/usr/bin/bash", + "TERM=xterm", + "USER=u0_a1", + "WAYLAND_DISPLAY=wayland-0", + "XDG_RUNTIME_DIR=/run/user/65534", + "XDG_SESSION_CLASS=user", + "XDG_SESSION_TYPE=wayland", + "PULSE_SERVER=unix:/run/user/65534/pulse/native", + }, + + FS: &testsuite.FS{Dir: dir{ + ".hakurei": {Mode: os.ModeDir | 0755, Dir: dir{ + "test-helper": {Mode: 0755}, + }}, + + "dev": {Mode: os.ModeDir | 0755, Dir: dir{ + "core": {Mode: os.ModeSymlink | 0777}, + "fd": {Mode: os.ModeSymlink | 0777}, + "full": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, + "mqueue": {Mode: os.ModeDir | os.ModeSticky | 0777, Dir: dir{}}, + "null": {Mode: os.ModeDevice | os.ModeCharDevice | 0666, Data: new("")}, + "ptmx": {Mode: os.ModeSymlink | 0777}, + "pts": {Mode: os.ModeDir | 0755, Dir: dir{ + "ptmx": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, + }}, + "random": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, + "shm": {Mode: os.ModeDir | os.ModeSticky | 0777, Dir: dir{}}, + "stderr": {Mode: os.ModeSymlink | 0777}, + "stdin": {Mode: os.ModeSymlink | 0777}, + "stdout": {Mode: os.ModeSymlink | 0777}, + "tty": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, + "urandom": {Mode: os.ModeDevice | os.ModeCharDevice | 0444}, + "zero": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, + }}, + + "etc": {Mode: os.ModeDir | 0755, Dir: dir{ + "passwd": {Mode: 0600, + Data: new("u0_a1:x:65534:65534:Hakurei:/.hakurei:/usr/bin/bash\n")}, + "group": {Mode: 0600, + Data: new("hakurei:x:65534:\n")}, + }}, + + "lib64": {Mode: os.ModeDir | 0755, Dir: dir{ + "ld-linux-x86-64.so.2": {Mode: os.ModeSymlink | 0777}, + }}, + + "run": {Mode: os.ModeDir | 0755, Dir: dir{ + "user": {Mode: os.ModeDir | 0755, Dir: dir{ + "65534": {Mode: os.ModeDir | 0700, Dir: dir{ + "bus": {Mode: os.ModeSocket | 0775}, + "wayland-0": {Mode: os.ModeSocket | 070}, + "pulse": {Mode: os.ModeDir | 0700, Dir: dir{ + "native": {Mode: os.ModeSocket | 0777}, + }}, + }}, + }}, + }}, + + "tmp": {Mode: os.ModeDir | os.ModeSticky | 0777, Dir: dir{}}, + + "lib": {Mode: os.ModeDir | 0755}, + "proc": {Mode: os.ModeDir | 0555}, + }}, + + Mount: []*mountinfo.Entry{ + r("/sysroot", "/", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110000,gid=110000,inode64"), + r("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"), + r("/", "/.hakurei", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,size=4k,mode=755,uid=110000,gid=110000,inode64"), + r("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", "rw,mode=755,uid=110000,gid=110000,inode64"), + r("/null", "/dev/null", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/zero", "/dev/zero", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/full", "/dev/full", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/random", "/dev/random", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/urandom", "/dev/urandom", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/tty", "/dev/tty", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"), + r("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"), + r("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,uid=110000,gid=110000,inode64"), + r("/", "/run/user", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,size=16384k,mode=755,uid=110000,gid=110000,inode64"), + r("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,uid=110000,gid=110000,inode64"), + r(ignore, "/etc/passwd", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110000,gid=110000,inode64"), + r(ignore, "/etc/group", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110000,gid=110000,inode64"), + r(ignore, "/run/user/65534/wayland-0", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r(ignore, "/run/user/65534/bus", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r("/usr/lib", "/lib", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r("/opt/test-helper/bin/tester", "/.hakurei/test-helper", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r(ignore, "/run/user/65534/pulse/native", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + }, + + Seccomp: true, + + TrySocket: "/tmp/.X11-unix/X0", + ErrnoAbstract: syscall.ECONNREFUSED, + ErrnoPathname: syscall.ENOENT, +}.register("simple") diff --git a/cmd/hakurei/testsuite/sandbox/testdata/sum.go b/cmd/hakurei/testsuite/sandbox/testdata/sum.go new file mode 100644 index 00000000..e4e8643a --- /dev/null +++ b/cmd/hakurei/testsuite/sandbox/testdata/sum.go @@ -0,0 +1,22 @@ +//go:build testsuite || tester + +package testdata + +import ( + "crypto/sha512" + "encoding/base64" + "strconv" +) + +// sum decodes s as [base64.StdEncoding] and panics if it is invalid or +// unexpectedly sized. +func sum(s string) [sha512.Size]byte { + p, err := base64.StdEncoding.DecodeString(s) + if err != nil { + panic(err) + } + if len(p) != sha512.Size { + panic("unexpected checksum sized " + strconv.Itoa(len(p))) + } + return ([sha512.Size]byte)(p) +} diff --git a/cmd/hakurei/testsuite/sandbox/testdata/sum_amd64.go b/cmd/hakurei/testsuite/sandbox/testdata/sum_amd64.go new file mode 100644 index 00000000..bd751105 --- /dev/null +++ b/cmd/hakurei/testsuite/sandbox/testdata/sum_amd64.go @@ -0,0 +1,9 @@ +//go:build testsuite || tester + +package testdata + +var ( + SumPD = sum("xpiwgf+Vev4XptlDdFN9N/KmP2+d112nVGVCQHqeMkduvaMxK6d4XX9hhUK8+vJ8on3MLd26hSBp0ovP6MrTmg==") + sumSimple = sum("6IApjfK9Z1HQBA/CG8DtTAD5XcDXulBsJE2LjPaGbbqO9KMylvKHtmzMwdeOlwJll/hMx97BVz4UiWD701zXNQ==") + sumTTY = sum("C3YAdHbByeJdv2dMKf32CaFlanAGPkkydlThtTYK09oG4aPjK/gOlhxVFq2D1Lnn6b3odqk3l+J2J9JVXCWFiw==") +) diff --git a/cmd/hakurei/testsuite/sandbox/testdata/sum_arm64.go b/cmd/hakurei/testsuite/sandbox/testdata/sum_arm64.go new file mode 100644 index 00000000..1691828f --- /dev/null +++ b/cmd/hakurei/testsuite/sandbox/testdata/sum_arm64.go @@ -0,0 +1,9 @@ +//go:build testsuite || tester + +package testdata + +var ( + SumPD = sum("QzzpuREoLW3MgCkxn7ebgWtg1aeV7I/JQ0TdAnYU1o8CMWapG7iB+q7u3Sbj2JR04UHlppqX6TuJhMqPFJmZgA==") + sumSimple = sum("eTGFOKPchRMUtr2W8Q1YYayyqn4Ty43gYZ0PanZwnWfwHvP9Z+GVhisC+XEeW3abxNHrT8DfxBpyPInJaKkylw==") + sumTTY = sum("zx9NyHQ2uo7JXSaLZjpjl7sLSlrGTYVX5sxSnYsPb2Xa06krYu0p2F7unG3eEmd1ek0PhgMuikXKG86t+jTPXg==") +) diff --git a/cmd/hakurei/testsuite/sandbox/testdata/testdata.go b/cmd/hakurei/testsuite/sandbox/testdata/testdata.go new file mode 100644 index 00000000..bdb5178e --- /dev/null +++ b/cmd/hakurei/testsuite/sandbox/testdata/testdata.go @@ -0,0 +1,125 @@ +//go:build testsuite || tester + +// Package testdata holds sandbox inspection test cases. +package testdata + +import ( + "crypto/sha512" + "iter" + "log" + "strconv" + "syscall" + + "hakurei.app/check" + "hakurei.app/fhs" + "hakurei.app/hst" + "hakurei.app/internal/testsuite" + "hakurei.app/internal/testsuite/mountinfo" +) + +// A TestCase represents a named test case that may be requested by the caller. +type TestCase struct { + // Configuration of the inspected container. + Hakurei hst.Config + // Checksum of expected seccomp filter program. + Sum [sha512.Size]byte + + // Expected environment. Skipped if nil. + Env []string `json:"env,omitempty"` + // Expected root filesystem. Skipped if nil. + FS *testsuite.FS `json:"fs,omitempty"` + // Expected mountinfo records. Skipped if nil. + Mount []*mountinfo.Entry `json:"mount,omitempty"` + // Whether to run seccomp checks. + Seccomp bool `json:"seccomp,omitempty"` + + // Name of pathname and abstract sockets to attempt. + TrySocket string `json:"try_socket,omitempty"` + // Errno to expect attempting to reach the abstract socket. + ErrnoAbstract syscall.Errno `json:"errno_abstract,omitempty"` + // Errno to expect attempting to reach the pathname socket. + ErrnoPathname syscall.Errno `json:"errno_pathname,omitempty"` +} + +// testCases hold all named test cases. +var testCases map[string]TestCase + +// fc returns c wrapped in its JSON adapter. +func fc(c hst.FilesystemConfig) hst.FilesystemConfigJSON { + return hst.FilesystemConfigJSON{ + FilesystemConfig: c, + } +} + +// ignore is the magic string for a mountinfo field to be ignored. +const ignore = "//ignore" + +type dir = map[string]*testsuite.FS + +// r returns the address of a [mountinfo.Entry]. +func r( + root, target, vfsOptstr string, + fsType, source, fsOptstr string, +) *mountinfo.Entry { + return &mountinfo.Entry{ + ID: -1, + Parent: -1, + Root: root, + Target: target, + VfsOptstr: vfsOptstr, + FsType: fsType, + Source: source, + FsOptstr: fsOptstr, + } +} + +var ( + // fcLinker is the dynamic linker symlink. + fcLinker = fc(&hst.FSLink{ + Target: fhs.AbsRoot.Append("lib64", "ld-linux-x86-64.so.2"), + Linkname: "../lib/x86_64-linux-gnu/ld-linux-x86-64.so.2", + }) + // fcLib is the dynamic library bind mount. + fcLib = fc(&hst.FSBind{Source: fhs.AbsRoot.Append("lib")}) + + // absTestHelper is the absolute pathname of the test helper program. + absTestHelper = hst.AbsPrivateTmp.Append("test-helper") + // fcTestHelper is the test helper bind mount. + fcTestHelper = fc(&hst.FSBind{ + Target: absTestHelper, + Source: check.MustAbs("/opt/test-helper/bin/tester"), + }) +) + +// register adds a test case to testCases. +func (c TestCase) register(name string) (_ struct{}) { + if testCases == nil { + testCases = make(map[string]TestCase) + } + + if _, ok := testCases[name]; ok { + panic("attempting to register " + strconv.Quote(name) + " twice") + } + testCases[name] = c + return +} + +// Get returns the named test case, or terminates the program if name is invalid. +func Get(name string) TestCase { + tc, ok := testCases[name] + if !ok { + log.Fatalf("invalid test case %q", name) + } + return tc +} + +// All returns an iterator over all named test cases. +func All() iter.Seq2[string, TestCase] { + return func(yield func(string, TestCase) bool) { + for name, tc := range testCases { + if !yield(name, tc) { + return + } + } + } +} diff --git a/cmd/hakurei/testsuite/sandbox/testdata/tty.go b/cmd/hakurei/testsuite/sandbox/testdata/tty.go new file mode 100644 index 00000000..2a3ba76e --- /dev/null +++ b/cmd/hakurei/testsuite/sandbox/testdata/tty.go @@ -0,0 +1,145 @@ +//go:build testsuite || tester + +package testdata + +import ( + "os" + + "hakurei.app/fhs" + "hakurei.app/hst" + "hakurei.app/internal/testsuite" + "hakurei.app/internal/testsuite/mountinfo" +) + +var _ = TestCase{ + Hakurei: hst.Config{ + ID: "app.hakurei.sample.tty", + Enablements: new(hst.EWayland | hst.EPipeWire | hst.EDBus | hst.EX11), + Identity: 2, + + Container: &hst.ContainerConfig{ + Hostname: "hakurei-sample-tty", + + Filesystem: []hst.FilesystemConfigJSON{ + fcLinker, + fcLib, + fcTestHelper, + }, + + Username: "u0_a2", + Shell: fhs.AbsUsrBin.Append("bash"), + Home: hst.AbsPrivateTmp, + Path: absTestHelper, + Args: []string{"tester", "tty"}, + + Flags: hst.FHostNet | hst.FHostAbstract | + hst.FTty | hst.FShareRuntime, + }, + }, + + // 0, PresetExt | PresetDenyNS | PresetDenyDevel + Sum: sumTTY, + + Env: []string{ + "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus", + "DISPLAY=:0", + "HOME=/.hakurei", + "SHELL=/usr/bin/bash", + "TERM=xterm", + "USER=u0_a2", + "WAYLAND_DISPLAY=wayland-0", + "XDG_RUNTIME_DIR=/run/user/65534", + "XDG_SESSION_CLASS=user", + "XDG_SESSION_TYPE=wayland", + "PULSE_SERVER=unix:/run/user/65534/pulse/native", + }, + + FS: &testsuite.FS{Dir: dir{ + ".hakurei": {Mode: os.ModeDir | 0755, Dir: dir{ + "test-helper": {Mode: 0755}, + }}, + + "dev": {Mode: os.ModeDir | 0755, Dir: dir{ + "core": {Mode: os.ModeSymlink | 0777}, + "fd": {Mode: os.ModeSymlink | 0777}, + "full": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, + "mqueue": {Mode: os.ModeDir | os.ModeSticky | 0777, Dir: dir{}}, + "null": {Mode: os.ModeDevice | os.ModeCharDevice | 0666, Data: new("")}, + "ptmx": {Mode: os.ModeSymlink | 0777}, + "pts": {Mode: os.ModeDir | 0755, Dir: dir{ + "ptmx": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, + }}, + "random": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, + "shm": {Mode: os.ModeDir | os.ModeSticky | 0777, Dir: dir{}}, + "stderr": {Mode: os.ModeSymlink | 0777}, + "stdin": {Mode: os.ModeSymlink | 0777}, + "stdout": {Mode: os.ModeSymlink | 0777}, + "tty": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, + "urandom": {Mode: os.ModeDevice | os.ModeCharDevice | 0444}, + "zero": {Mode: os.ModeDevice | os.ModeCharDevice | 0666}, + }}, + + "etc": {Mode: os.ModeDir | 0755, Dir: dir{ + "passwd": {Mode: 0600, + Data: new("u0_a2:x:65534:65534:Hakurei:/.hakurei:/usr/bin/bash\n")}, + "group": {Mode: 0600, + Data: new("hakurei:x:65534:\n")}, + }}, + + "lib64": {Mode: os.ModeDir | 0755, Dir: dir{ + "ld-linux-x86-64.so.2": {Mode: os.ModeSymlink | 0777}, + }}, + + "run": {Mode: os.ModeDir | 0755, Dir: dir{ + "user": {Mode: os.ModeDir | 0755, Dir: dir{ + "65534": {Mode: os.ModeDir | 0770, Dir: dir{ + "bus": {Mode: os.ModeSocket | 0775}, + "wayland-0": {Mode: os.ModeSocket | 070}, + "pulse": {Mode: os.ModeDir | 0700, Dir: dir{ + "native": {Mode: os.ModeSocket | 0777}, + }}, + }}, + }}, + }}, + + "tmp": {Mode: os.ModeDir | os.ModeSticky | 0777, Dir: dir{ + ".X11-unix": {Mode: os.ModeDir | 0755, Dir: dir{ + "X0": {Mode: os.ModeSocket | 0775}, + }}, + }}, + + "lib": {Mode: os.ModeDir | 0755}, + "proc": {Mode: os.ModeDir | 0555}, + }}, + + Mount: []*mountinfo.Entry{ + r("/sysroot", "/", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110001,gid=110001,inode64"), + r("/", "/proc", "rw,nosuid,nodev,noexec,relatime", "proc", "proc", "rw"), + r("/", "/.hakurei", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,size=4k,mode=755,uid=110001,gid=110001,inode64"), + r("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", "rw,mode=755,uid=110001,gid=110001,inode64"), + r("/null", "/dev/null", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/zero", "/dev/zero", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/full", "/dev/full", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/random", "/dev/random", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/urandom", "/dev/urandom", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/tty", "/dev/tty", "rw,nosuid,noexec,relatime", "devtmpfs", "devtmpfs", ignore), + r("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"), + r("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"), + r("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,uid=110001,gid=110001,inode64"), + r("/", "/run/user", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,size=16384k,mode=755,uid=110001,gid=110001,inode64"), + r("/tmp/hakurei.0/runtime/2", "/run/user/65534", "rw,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r("/", "/tmp", "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", "rw,uid=110001,gid=110001,inode64"), + r(ignore, "/etc/passwd", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110001,gid=110001,inode64"), + r(ignore, "/etc/group", "ro,nosuid,nodev,relatime", "tmpfs", "rootfs", "rw,uid=110001,gid=110001,inode64"), + r(ignore, "/run/user/65534/wayland-0", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r("/tmp/.X11-unix", "/tmp/.X11-unix", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r(ignore, "/run/user/65534/bus", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r("/usr/lib", "/lib", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r("/opt/test-helper/bin/tester", "/.hakurei/test-helper", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + r(ignore, "/run/user/65534/pulse/native", "ro,nosuid,nodev,relatime", "overlay", "overlay", ignore), + }, + + Seccomp: true, + + TrySocket: "/tmp/.X11-unix/X0", +}.register("tty") diff --git a/cmd/hakurei/testsuite/sandbox/tester/main.go b/cmd/hakurei/testsuite/sandbox/tester/main.go new file mode 100644 index 00000000..79d17c79 --- /dev/null +++ b/cmd/hakurei/testsuite/sandbox/tester/main.go @@ -0,0 +1,213 @@ +//go:build tester + +// The sandbox tester runs within a cmd/hakurei container and validates its +// state. Since the test environment is relatively predictable, the tester can +// make various assumptions about the host. +package main + +import ( + "errors" + "log" + "net" + "os" + "os/signal" + "path/filepath" + "syscall" + + "hakurei.app/cmd/hakurei/testsuite/sandbox/testdata" + "hakurei.app/internal/testsuite/mountinfo" +) + +//#include <sys/quota.h> +import "C" + +// mustAbs returns s, or terminates the program if s is not absolute. +func mustAbs(s string) string { + if !filepath.IsAbs(s) { + log.Fatalf("%q is not absolute", s) + } + return s +} + +func main() { + log.SetFlags(0) + log.SetPrefix("tester: ") + + if len(os.Args) != 2 { + log.Fatal("tester requires 1 argument") + } + want := testdata.Get(os.Args[1]) + log.SetPrefix("tester: " + os.Args[1] + " ") + + checkWritableDirPaths := []string{ + "/dev/shm", + "/tmp", + os.Getenv("XDG_RUNTIME_DIR"), + } + for _, a := range checkWritableDirPaths { + pathname := filepath.Join(mustAbs(a), ".hakurei-check") + if err := os.WriteFile(pathname, make([]byte, 1<<8), 0600); err != nil { + log.Fatalf("[FAIL] %s", err) + } else if err = os.Remove(pathname); err != nil { + log.Fatalf("[FAIL] %s", err) + } else { + log.Printf("[ OK ] %s is writable", a) + } + } + + if want.Env != nil { + var ( + fail bool + i int + got string + ) + for i, got = range os.Environ() { + if i == len(want.Env) { + log.Fatalf("got more than %d environment variables", len(want.Env)) + } + if got != want.Env[i] { + fail = true + log.Printf("[FAIL] %s", got) + } else { + log.Printf("[ OK ] %s", got) + } + } + + i++ + if i != len(want.Env) { + log.Fatalf("got %d environment variables, want %d", i, len(want.Env)) + } + + if fail { + log.Fatalf("[FAIL] some environment variables did not match") + } + } else { + log.Printf("[SKIP] skipping environ check") + } + + if want.FS != nil { + if err := want.FS.Compare(log.Printf, ".", os.DirFS("/")); err != nil { + log.Fatalf("%v", err) + } + } else { + log.Printf("[SKIP] skipping fs check") + } + + if want.Mount != nil { + var fail bool + + m, err := mountinfo.Open("") + if err != nil { + log.Fatal(err) + } + + i := 0 + var ent mountinfo.Entry + for m.Next() { + m.Copy(&ent) + + if i == len(want.Mount) { + log.Fatalf("got more than %d entries", i) + } + if !ent.EqualWithIgnore(want.Mount[i], "//ignore") { + fail = true + log.Printf("[FAIL] %s", &ent) + } else { + log.Printf("[ OK ] %s", &ent) + } + + i++ + } + if err = m.Err(); err != nil { + log.Fatalf("%v", err) + } + + if i != len(want.Mount) { + log.Fatalf("got %d entries, want %d", i, len(want.Mount)) + } + + if fail { + log.Fatalf("[FAIL] some mount points did not match") + } + } else { + log.Printf("[SKIP] skipping mounts check") + } + + if want.Seccomp { + const NULL = 0 + + for _, tc := range []struct { + name string + errno syscall.Errno + + trap, a1, a2, a3, a4, a5, a6 uintptr + }{ + {"syslog", syscall.EPERM, syscall.SYS_SYSLOG, 0, NULL, NULL, NULL, NULL, NULL}, + {"acct", syscall.EPERM, syscall.SYS_ACCT, 0, NULL, NULL, NULL, NULL, NULL}, + {"quotactl", syscall.EPERM, syscall.SYS_QUOTACTL, C.Q_GETQUOTA, NULL, uintptr(os.Getuid()), NULL, NULL, NULL}, + {"add_key", syscall.EPERM, syscall.SYS_ADD_KEY, NULL, NULL, NULL, NULL, NULL, NULL}, + {"keyctl", syscall.EPERM, syscall.SYS_KEYCTL, NULL, NULL, NULL, NULL, NULL, NULL}, + {"request_key", syscall.EPERM, syscall.SYS_REQUEST_KEY, NULL, NULL, NULL, NULL, NULL, NULL}, + {"move_pages", syscall.EPERM, syscall.SYS_MOVE_PAGES, uintptr(os.Getpid()), NULL, NULL, NULL, NULL, NULL}, + {"mbind", syscall.EPERM, syscall.SYS_MBIND, NULL, NULL, NULL, NULL, NULL, NULL}, + {"get_mempolicy", syscall.EPERM, syscall.SYS_GET_MEMPOLICY, NULL, NULL, NULL, NULL, NULL, NULL}, + {"set_mempolicy", syscall.EPERM, syscall.SYS_SET_MEMPOLICY, NULL, NULL, NULL, NULL, NULL, NULL}, + {"migrate_pages", syscall.EPERM, syscall.SYS_MIGRATE_PAGES, NULL, NULL, NULL, NULL, NULL, NULL}, + } { + if _, _, errno := syscall.Syscall6(tc.trap, tc.a1, tc.a2, tc.a3, tc.a4, tc.a5, tc.a6); errno != tc.errno { + log.Fatalf("[FAIL] %s: %v, want %v", tc.name, errno, tc.errno) + } + log.Printf("[ OK ] %s: %v", tc.name, tc.errno) + } + } else { + log.Printf("[SKIP] skipping seccomp check") + } + + if want.TrySocket != "" { + abstractConn, abstractErr := net.Dial("unix", "@"+want.TrySocket) + pathnameConn, pathnameErr := net.Dial("unix", want.TrySocket) + ok := true + + if abstractErr == nil { + if err := abstractConn.Close(); err != nil { + ok = false + log.Printf("Close: %v", err) + } + } + if pathnameErr == nil { + if err := pathnameConn.Close(); err != nil { + ok = false + log.Printf("Close: %v", err) + } + } + + abstractWantErr := error(want.ErrnoAbstract) + pathnameWantErr := error(want.ErrnoPathname) + if want.ErrnoAbstract == 0 { + abstractWantErr = nil + } + if want.ErrnoPathname == 0 { + pathnameWantErr = nil + } + + if !errors.Is(abstractErr, abstractWantErr) { + ok = false + log.Printf("abstractErr: %v, want %v", abstractErr, abstractWantErr) + } + if !errors.Is(pathnameErr, pathnameWantErr) { + ok = false + log.Printf("pathnameErr: %v, want %v", pathnameErr, pathnameWantErr) + } + + if !ok { + os.Exit(1) + } + } + + s := make(chan os.Signal, 1) + signal.Notify(s, syscall.SIGTERM) + if _, err := os.Stdout.Write(make([]byte, 8)); err != nil { + log.Fatalf("cannot notify testsuite: %v", err) + } + <-s +} diff --git a/cmd/mbf/cache.go b/cmd/mbf/cache.go index 0e20ca99..166e1c45 100644 --- a/cmd/mbf/cache.go +++ b/cmd/mbf/cache.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "net/http" "os" "path/filepath" @@ -14,6 +15,9 @@ import ( "hakurei.app/pkg" ) +// poisonOpen is whether cache.open is poisoned. This enables running as root. +var _, poisonOpen = os.LookupEnv("MBF_POISON_OPEN") + // cache refers to an instance of [pkg.Cache] that might be open. type cache struct { ctx context.Context @@ -49,6 +53,10 @@ func writeTitle(msg message.Msg, s string) { // open opens the underlying [pkg.Cache]. func (cache *cache) open() (err error) { + if poisonOpen { + return errors.New("attempting to open cache") + } + if cache.c != nil { return os.ErrInvalid } diff --git a/cmd/mbf/internal/ci/ci.go b/cmd/mbf/internal/ci/ci.go new file mode 100644 index 00000000..b1ee6a8c --- /dev/null +++ b/cmd/mbf/internal/ci/ci.go @@ -0,0 +1,569 @@ +// Package ci implements the CI service and client. +package ci + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "crypto/ed25519" + "encoding/binary" + "errors" + "io" + "io/fs" + "log" + "net" + "net/http" + "os" + "path" + "path/filepath" + "sync" + "syscall" + "time" + "unique" + "unsafe" + _ "unsafe" // for go:linkname + + "hakurei.app/internal/rosa" + "hakurei.app/message" + "hakurei.app/pkg" +) + +// setSource is made available here to accept prepared hakurei tarballs. +// +//go:linkname setSource hakurei.app/internal/rosa.(*S).setSource +func setSource(s *rosa.S, p []byte, version string) + +// inotifyInit returns a new inotify instance. +func inotifyInit() (*os.File, error) { + fd, err := syscall.InotifyInit1(syscall.IN_NONBLOCK | syscall.IN_CLOEXEC) + if err != nil { + return nil, os.NewSyscallError("inotify_init1", err) + } + return os.NewFile(uintptr(fd), "inotify"), nil +} + +// inotifyAddWatch adds pathname to in. +func inotifyAddWatch( + in *os.File, + pathname string, + mask uint32, +) (watchdesc int, err error) { + sc, _err := in.SyscallConn() + if _err != nil { + return -1, _err + } + if _err = sc.Control(func(fd uintptr) { + watchdesc, err = syscall.InotifyAddWatch(int(fd), pathname, mask) + }); _err != nil { + return -1, _err + } + return +} + +// Follow reads from the file at pathname and writes its contents and any new +// contents to w. follow returns if a read, write or inotify error occurs, or +// the context is cancelled. +func Follow(ctx context.Context, pathname string, w io.Writer) (err error) { + var in *os.File + if in, err = inotifyInit(); err != nil { + return + } + defer func() { + if _err := in.Close(); err == nil { + err = _err + } + }() + + if _, err = inotifyAddWatch(in, pathname, syscall.IN_MODIFY); err != nil { + return + } + var r *os.File + if r, err = os.Open(pathname); err != nil { + return + } + defer func() { + if _err := r.Close(); err == nil { + err = _err + } + }() + + done := make(chan struct{}) + defer close(done) + go func() { + select { + case <-ctx.Done(): + now := time.Now() + _ = in.SetDeadline(now) + return + + case <-done: + return + } + }() + + if _, err = io.Copy(w, r); err != nil { + return + } + + buf := make([]byte, os.Getpagesize()) + for { + if _, err = io.Copy(w, r); err != nil { + return + } + if _, err = in.Read(buf); err != nil { + if errors.Is(err, os.ErrDeadlineExceeded) { + err = nil + } + return + } + } +} + +// archiveTime is the hardcoded time written to every header time field. +var archiveTime = time.Unix(0, 0) + +// Path wraps the [pkg.Cache] pathname. +type Path string + +// String returns the value of p. +func (p Path) String() string { return string(p) } + +// append is [filepath.Join] with p as the first element. +func (p Path) append(elem ...string) string { + return filepath.Join(append([]string{p.String()}, elem...)...) +} + +// New returns a new [Path]. +func New(c *pkg.Cache) Path { return Path(c.Path().String()) } + +// name returns the CI socket pathname. +func (p Path) name() string { return p.append("ci") } + +// client returns the CI http client. +func (p Path) client() *http.Client { + var d net.Dialer + addr := net.UnixAddr{ + Net: "unix", + Name: p.name(), + } + + return &http.Client{Transport: &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + return d.DialUnix(ctx, "unix", nil, &addr) + }, + }} +} + +// cure writes the identifier of the pending artifact, cures the artifact, and +// writes the cure whence. For an unsuccessful cure, a negative whence is +// written, followed by a user-facing error string. +func cure(c *pkg.Cache, a pkg.Artifact, w http.ResponseWriter) error { + h := w.Header() + h.Set("Content-Type", "application/octet-stream") + h.Set("Cache-Control", "no-cache") + + f, ok := w.(http.Flusher) + if !ok { + _, _ = w.Write([]byte{0}) + return errors.ErrUnsupported + } + + id := c.Ident(a).Value() + if _, err := w.Write(id[:]); err != nil { + return err + } + f.Flush() + + _, _, whence, err := c.CureWhence(a) + if err != nil { + whence = -1 + } + if _, _err := w.Write( + binary.LittleEndian.AppendUint64(nil, uint64(whence)), + ); _err != nil { + return _err + } + + if err != nil { + if _, _err := io.WriteString(w, err.Error()); _err != nil { + return _err + } + } + f.Flush() + return err +} + +// spool holds reusable [rosa.S] instances. +var spool = sync.Pool{New: func() any { return rosa.New() }} + +// getS returns the address of a populated [rosa.S]. Its hakurei-source may be +// clobbered and must be replaced using setSource before use. +func getS() *rosa.S { return spool.Get().(*rosa.S) } + +// putS returns s to spool. +func putS(s *rosa.S) { spool.Put(s) } + +// versionSize is the maximum size of the specified version string, plus its +// deliminator byte. +const versionSize = 8 + 16 + 6 + 2 + +// errBadVersion is returned by readSource if a header does not contain +// the deliminator byte. +var errBadVersion = errors.New("unterminated version string") + +// readSource reads a version string and compressed source tarball from r and +// returns the address of a [rosa.S] with this source tarball. The resulting +// [rosa.S] must be returned via putS. +func readSource(w http.ResponseWriter, r *http.Request) (*rosa.S, error) { + var header [versionSize]byte + _, err := io.ReadFull(r.Body, header[:]) + if err != nil { + _ = r.Body.Close() + http.Error(w, "bad header", http.StatusBadRequest) + return nil, err + } + + var version string + if i := bytes.IndexByte(header[:], 0); i < 0 { + _ = r.Body.Close() + http.Error(w, "unterminated version string", http.StatusBadRequest) + return nil, errBadVersion + } else { + version = unsafe.String(&header[0], i) + } + + var p []byte + if p, err = io.ReadAll(r.Body); err != nil { + _ = r.Body.Close() + http.Error(w, "cannot receive payload", http.StatusInternalServerError) + return nil, err + } + + s := getS() + setSource(s, p, version) + return s, r.Body.Close() +} + +// ErrDaemonError is returned by writeSource generally if cure could not flush +// on the connection to notify completion. +var ErrDaemonError = errors.New("CI service could not process the request") + +// writeSource writes a source tarball to the specified endpoint of the CI +// backend servicing the cache referred to by cm. +func (p Path) writeSource( + ctx context.Context, + w io.Writer, + endpoint, source, version string, +) (*pkg.ID, error) { + if len(version) >= versionSize { + return nil, syscall.ENOMEM + } + var header [versionSize]byte + copy(header[:], version[:]) + + var buf bytes.Buffer + gw := gzip.NewWriter(&buf) + tw := tar.NewWriter(gw) + + if err := filepath.WalkDir(source, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() && d.Name() == ".git" { + return fs.SkipDir + } + + var fi fs.FileInfo + if fi, err = d.Info(); err != nil { + return err + } + + var linkname string + if fi.Mode()&fs.ModeSymlink != 0 { + if linkname, err = os.Readlink(path); err != nil { + return err + } + } + + var h *tar.Header + if h, err = tar.FileInfoHeader(fi, linkname); err != nil { + return err + } + h.AccessTime, h.ChangeTime, h.ModTime = archiveTime, archiveTime, archiveTime + h.Name = path + + var isVersion bool + if dir, file := filepath.Split(path); filepath.Base(dir) == "dist" && + file == "VERSION" && fi.Mode().IsRegular() { + isVersion = true + h.Size = int64(len(version)) + } + + if err = tw.WriteHeader(h); err != nil { + return err + } + + if isVersion { + _, err = io.WriteString(tw, version) + return err + } + + if fi.Mode().IsRegular() { + var f io.ReadCloser + if f, err = os.Open(path); err != nil { + return err + } + + _, err = io.Copy(tw, f) + if _err := f.Close(); err == nil { + err = _err + } + if err != nil { + return err + } + } + return nil + }); err != nil { + return nil, err + } + if err := tw.Close(); err != nil { + return nil, err + } + if err := gw.Close(); err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + "http://"+path.Join("_", endpoint), + io.MultiReader(bytes.NewReader(header[:]), bytes.NewReader(buf.Bytes())), + ) + if err != nil { + return nil, err + } + + var resp *http.Response + resp, err = p.client().Do(req) + if err != nil { + return nil, err + } + + var id pkg.ID + _, err = io.ReadFull(resp.Body, id[:]) + if err != nil { + _ = resp.Body.Close() + if errors.Is(err, io.ErrUnexpectedEOF) { + return nil, ErrDaemonError + } + return nil, err + } + + c, cancel := context.WithCancel(ctx) + done := make(chan error, 1) + var whence int + go func() { + defer cancel() + var wbuf [8]byte + _, _err := io.ReadFull(resp.Body, wbuf[:]) + whence = int(binary.LittleEndian.Uint64(wbuf[:])) + done <- _err + }() + + if w != nil { + retry: + err = Follow(c, filepath.Join(p.String(), "status", pkg.Encode(id)), w) + if err != nil { + if ctx.Err() == nil && errors.Is(err, os.ErrNotExist) { + goto retry + } + + _ = resp.Body.Close() + return nil, err + } + } + + err = <-done + if err != nil { + _ = resp.Body.Close() + return nil, err + } + + if whence < 0 { + var m []byte + if m, err = io.ReadAll(resp.Body); err != nil { + _ = resp.Body.Close() + return nil, err + } else if err = resp.Body.Close(); err != nil { + return nil, err + } + return nil, errors.New(unsafe.String(unsafe.SliceData(m), len(m))) + } + return &id, resp.Body.Close() +} + +// The stubKey is used by the mirror service exposed by serve where +// authentication is unnecessary. +var stubKey = ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) + +// fetch fetches the outcome of id and writes it to the specified directory. +func (p Path) fetch(ctx context.Context, id *pkg.ID, output string) error { + c := p.client() + r, err := rosa.NewRemote( + c, "http://_", + stubKey.Public().(ed25519.PublicKey), + ) + if err != nil { + return err + } + + var sum *pkg.Checksum + if sum, err = r.Artifact(ctx, unique.Make(*id)); err != nil { + return err + } + + var req *http.Request + if req, err = http.NewRequestWithContext( + ctx, + http.MethodGet, + "http://"+path.Join("_", "outcome", pkg.Encode(*sum)), + nil, + ); err != nil { + return err + } + + var resp *http.Response + if resp, err = c.Do(req); err != nil { + return err + } + + err = pkg.Extract(resp.Body, output, nil) + if closeErr := resp.Body.Close(); err == nil { + err = closeErr + } + return err +} + +const ( + // endpointDist accepts a Path.writeSource stream and produces a + // distribution for the submitted source. + endpointDist = "/dist" + + // endpointRace is like endpointDist, but the resulting distribution is + // instrumented with the data race detector. + endpointRace = "/race" +) + +// MakeDist creates a hakurei distribution using the CI service. +func (p Path) MakeDist( + ctx context.Context, + w io.Writer, + output, source, version string, +) error { + id, err := p.writeSource(ctx, w, endpointDist, source, version) + if err != nil { + return err + } + return p.fetch(ctx, id, output) +} + +// MakeDistRace creates a hakurei distribution (race) using the CI service. +func (p Path) MakeDistRace( + ctx context.Context, + w io.Writer, + output, source, version string, +) error { + id, err := p.writeSource(ctx, w, endpointRace, source, version) + if err != nil { + return err + } + return p.fetch(ctx, id, output) +} + +// Serve services CI workload dispatched to c. +func Serve(ctx context.Context, msg message.Msg, c *pkg.Cache) error { + const shutdownTimeout = 15 * time.Second + p := New(c) + addr := net.UnixAddr{ + Net: "unix", + Name: p.name(), + } + + var mux http.ServeMux + mux.HandleFunc("POST "+endpointDist, func(w http.ResponseWriter, r *http.Request) { + s, err := readSource(w, r) + if err != nil { + msg.Verbose(err) + return + } + defer putS(s) + + _, a := s.Std().MustLoad(rosa.H("hakurei-dist")) + if err = cure(c, a, w); err != nil { + msg.Verbose(err) + return + } + if msg.IsVerbose() { + msg.Verbosef( + "satisfied distribution %s", + pkg.Encode(c.Ident(a).Value()), + ) + } + }) + mux.HandleFunc("POST "+endpointRace, func(w http.ResponseWriter, r *http.Request) { + s, err := readSource(w, r) + if err != nil { + msg.Verbose(err) + return + } + defer putS(s) + + _, a := s.Std().MustLoad(rosa.H("hakurei-dist-race")) + if err = cure(c, a, w); err != nil { + msg.Verbose(err) + return + } + if msg.IsVerbose() { + msg.Verbosef( + "satisfied distribution %s (race)", + pkg.Encode(c.Ident(a).Value()), + ) + } + }) + + if r, err := os.OpenRoot(p.String()); err != nil { + return err + } else { + defer func() { + if err = r.Close(); err != nil { + msg.Verbose(err) + } + }() + rosa.NewMirror(msg, r.FS(), stubKey).Register(&mux) + } + + server := http.Server{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) + } + }() + + ul, err := net.ListenUnix("unix", &addr) + if err != nil { + return err + } + ul.SetUnlinkOnClose(true) + msg.Verbosef("listening on %s", addr.Name) + + err = server.Serve(ul) + if errors.Is(err, http.ErrServerClosed) { + err = nil + } + return err +} diff --git a/cmd/mbf/internal/ci/ci_test.go b/cmd/mbf/internal/ci/ci_test.go new file mode 100644 index 00000000..447c4ab1 --- /dev/null +++ b/cmd/mbf/internal/ci/ci_test.go @@ -0,0 +1,56 @@ +package ci_test + +import ( + "bytes" + "context" + "os" + "path/filepath" + "testing" + + "hakurei.app/cmd/mbf/internal/ci" +) + +func TestFollow(t *testing.T) { + t.Parallel() + + pathname := filepath.Join(t.TempDir(), "f") + w, err := os.Create(pathname) + if err != nil { + t.Fatal(err) + } + + var buf bytes.Buffer + ctx, cancel := context.WithCancel(t.Context()) + + var want string + go func() { + defer cancel() + for _, s := range []string{ + "\xde\xad\xbe\xef", + "\xff\xff\xff\xff", + "\x00\x00", + } { + want += s + if _, _err := w.WriteString(s); _err != nil { + panic(_err) + } + } + }() + +retry: + if err = ci.Follow(ctx, pathname, &buf); err != nil { + t.Fatal(err) + } + <-ctx.Done() + + // the inotify event takes time to arrive, and there is no way to + // synchronise for this cleanly + if buf.Len() != len(want) { + buf.Reset() + goto retry + } + + if got := buf.String(); got != want { + t.Fatalf("follow: %q, want %q", got, want) + } +} diff --git a/cmd/mbf/internal/pkgserver/api.go b/cmd/mbf/internal/pkgsite/api.go index 68ccd471..a3094359 100644 --- a/cmd/mbf/internal/pkgserver/api.go +++ b/cmd/mbf/internal/pkgsite/api.go @@ -1,5 +1,5 @@ -// Package pkgserver implements the package metadata service backend. -package pkgserver +// Package pkgsite implements the package metadata website. +package pkgsite import ( "context" diff --git a/cmd/mbf/internal/pkgserver/api_test.go b/cmd/mbf/internal/pkgsite/api_test.go index 1552fec9..d2c21d81 100644 --- a/cmd/mbf/internal/pkgserver/api_test.go +++ b/cmd/mbf/internal/pkgsite/api_test.go @@ -1,4 +1,4 @@ -package pkgserver +package pkgsite import ( "net/http" diff --git a/cmd/mbf/internal/pkgserver/index.go b/cmd/mbf/internal/pkgsite/index.go index 2a9b6de5..760806c0 100644 --- a/cmd/mbf/internal/pkgserver/index.go +++ b/cmd/mbf/internal/pkgsite/index.go @@ -1,4 +1,4 @@ -package pkgserver +package pkgsite import ( "cmp" diff --git a/cmd/mbf/internal/pkgserver/index_test.go b/cmd/mbf/internal/pkgsite/index_test.go index 8f3b5530..abf78876 100644 --- a/cmd/mbf/internal/pkgserver/index_test.go +++ b/cmd/mbf/internal/pkgsite/index_test.go @@ -1,4 +1,4 @@ -package pkgserver +package pkgsite import ( "bytes" diff --git a/cmd/mbf/internal/pkgserver/search.go b/cmd/mbf/internal/pkgsite/search.go index 15947804..ef7cd76f 100644 --- a/cmd/mbf/internal/pkgserver/search.go +++ b/cmd/mbf/internal/pkgsite/search.go @@ -1,4 +1,4 @@ -package pkgserver +package pkgsite import ( "cmp" diff --git a/cmd/mbf/internal/pkgserver/ui/index.html b/cmd/mbf/internal/pkgsite/ui/index.html index 6a5d72b4..6a5d72b4 100644 --- a/cmd/mbf/internal/pkgserver/ui/index.html +++ b/cmd/mbf/internal/pkgsite/ui/index.html diff --git a/cmd/mbf/internal/pkgserver/ui/index.ts b/cmd/mbf/internal/pkgsite/ui/index.ts index 0784b81f..0784b81f 100644 --- a/cmd/mbf/internal/pkgserver/ui/index.ts +++ b/cmd/mbf/internal/pkgsite/ui/index.ts diff --git a/cmd/mbf/internal/pkgserver/ui/style.css b/cmd/mbf/internal/pkgsite/ui/style.css index b4f281ac..b4f281ac 100644 --- a/cmd/mbf/internal/pkgserver/ui/style.css +++ b/cmd/mbf/internal/pkgsite/ui/style.css diff --git a/cmd/mbf/internal/pkgserver/ui/tsconfig.json b/cmd/mbf/internal/pkgsite/ui/tsconfig.json index 24df4936..24df4936 100644 --- a/cmd/mbf/internal/pkgserver/ui/tsconfig.json +++ b/cmd/mbf/internal/pkgsite/ui/tsconfig.json diff --git a/cmd/mbf/internal/pkgserver/ui/ui.go b/cmd/mbf/internal/pkgsite/ui/ui.go index 3fc0d89c..3fc0d89c 100644 --- a/cmd/mbf/internal/pkgserver/ui/ui.go +++ b/cmd/mbf/internal/pkgsite/ui/ui.go diff --git a/cmd/mbf/internal/pkgserver/ui/ui_full.go b/cmd/mbf/internal/pkgsite/ui/ui_full.go index 564787dc..564787dc 100644 --- a/cmd/mbf/internal/pkgserver/ui/ui_full.go +++ b/cmd/mbf/internal/pkgsite/ui/ui_full.go diff --git a/cmd/mbf/internal/pkgserver/ui/ui_stub.go b/cmd/mbf/internal/pkgsite/ui/ui_stub.go index daf010f8..daf010f8 100644 --- a/cmd/mbf/internal/pkgserver/ui/ui_stub.go +++ b/cmd/mbf/internal/pkgsite/ui/ui_stub.go diff --git a/cmd/mbf/main.go b/cmd/mbf/main.go index 4f471d6a..dd5ee624 100644 --- a/cmd/mbf/main.go +++ b/cmd/mbf/main.go @@ -43,8 +43,9 @@ import ( "hakurei.app/message" "hakurei.app/pkg" - "hakurei.app/cmd/mbf/internal/pkgserver" - "hakurei.app/cmd/mbf/internal/pkgserver/ui" + "hakurei.app/cmd/mbf/internal/ci" + "hakurei.app/cmd/mbf/internal/pkgsite" + "hakurei.app/cmd/mbf/internal/pkgsite/ui" ) // builtin contains native and embedded [rosa.Artifact] registrations. @@ -70,7 +71,7 @@ func main() { log.SetPrefix("mbf: ") msg := message.New(log.Default()) - if os.Geteuid() == 0 { + if !poisonOpen && os.Geteuid() == 0 { log.Fatal("this program must not run as root") } @@ -343,7 +344,7 @@ func main() { var mux http.ServeMux ui.Register(&mux) - if err = pkgserver.Register(ctx, &mux, r); err != nil { + if err = pkgsite.Register(ctx, &mux, r); err != nil { return } @@ -504,7 +505,7 @@ func main() { c.NewCommand( "daemon", "Service artifact IR with Rosa OS extensions", - func(args []string) error { + func([]string) error { ul, err := net.ListenUnix("unix", &addr) if err != nil { return err @@ -514,6 +515,63 @@ func main() { }, ) + _ci := c.New("ci", command.UsageInternal) + _ci.NewCommand( + "daemon", + "Service CI workload dispatched through the socket", + func([]string) error { + return cm.Do(func(cache *pkg.Cache) error { + return ci.Serve(ctx, msg, cache) + }) + }, + ) + { + var flagOutput string + _ci.NewCommand( + "dist", + "Request distribution tarball for the specified source directory", + func(args []string) error { + if len(args) != 2 { + return errors.New("dist requires 2 arguments") + } + + return ci.Path(cm.base).MakeDist( + ctx, + os.Stdout, + flagOutput, + args[0], args[1], + ) + }, + ).Flag( + &flagOutput, + "o", command.StringFlag("."), + "Write the resulting distribution to the named directory", + ) + } + { + var flagOutput string + _ci.NewCommand( + "race", + "Request distribution tarball (race) for the specified source directory", + func(args []string) error { + if len(args) != 2 { + return errors.New("race requires 2 arguments") + } + + return ci.Path(cm.base).MakeDistRace( + ctx, + os.Stdout, + flagOutput, + args[0], args[1], + ) + }, + ).Flag( + &flagOutput, + "o", command.StringFlag("."), + "Write the resulting distribution to the named directory", + ) + } + c.NewCommand( "keygen", "Create keypair for local cache", diff --git a/cmd/sharefs/fuse-operations.c b/cmd/sharefs/fuse-operations.c index ecff1ad6..36417126 100644 --- a/cmd/sharefs/fuse-operations.c +++ b/cmd/sharefs/fuse-operations.c @@ -14,269 +14,301 @@ /* 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) + 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) +#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 */ - } +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 */ + 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; + 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); +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; + (void)fi; - if (fstatat(priv->dirfd, pathname, statbuf, AT_SYMLINK_NOFOLLOW) == -1) - return -errno; - return impl_getattr(ctx, statbuf); + 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; +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); + struct fuse_context *ctx; + struct sharefs_private *priv; + GET_CONTEXT_PRIV(ctx, priv); + MUST_TRANSLATE_PATHNAME(pathname); - (void)offset; - (void)fi; + (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; - } + 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; - } + 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; + 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); + 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; - } + 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 */ + errno = 0; /* for the next readdir call */ + } + if (ret == 0 && errno != 0) + ret = -errno; /* readdir */ - closedir(dp); - return ret; + 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); +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; + (void)mode; - if (mkdirat(priv->dirfd, pathname, SHAREFS_PERM_DIR) == -1) - return -errno; - return 0; + 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); +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; + 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); +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; + 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); +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; + /* 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; +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); + struct fuse_context *ctx; + struct sharefs_private *priv; + GET_CONTEXT_PRIV(ctx, priv); + MUST_TRANSLATE_PATHNAME(pathname); - (void)fi; + (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; + 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); +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; + (void)fi; - if (utimensat(priv->dirfd, pathname, times, AT_SYMLINK_NOFOLLOW) == -1) - return -errno; - return 0; + 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; +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); + struct fuse_context *ctx; + struct sharefs_private *priv; + GET_CONTEXT_PRIV(ctx, priv); + MUST_TRANSLATE_PATHNAME(pathname); - (void)mode; + (void)mode; - if ((fd = openat(priv->dirfd, pathname, fi->flags & ~SHAREFS_FORBIDDEN_FLAGS, SHAREFS_PERM_REG)) == -1) - return -errno; - fi->fh = fd; - return 0; + 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; +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); + 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; + 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; +int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset, + struct fuse_file_info *fi) +{ + int ret; - (void)pathname; + (void)pathname; - if ((ret = pread(fi->fh, buf, count, offset)) == -1) - return -errno; - return ret; + 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; +int sharefs_write(const char *pathname, const char *buf, size_t count, + off_t offset, struct fuse_file_info *fi) +{ + int ret; - (void)pathname; + (void)pathname; - if ((ret = pwrite(fi->fh, buf, count, offset)) == -1) - return -errno; - return ret; + 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; +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); + 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; + 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; +int sharefs_release(const char *pathname, struct fuse_file_info *fi) +{ + (void)pathname; - return close(fi->fh); + return close(fi->fh); } -int sharefs_fsync(const char *pathname, int datasync, struct fuse_file_info *fi) { - (void)pathname; +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; + if (datasync ? fdatasync(fi->fh) : fsync(fi->fh) == -1) + return -errno; + return 0; } diff --git a/cmd/sharefs/fuse-operations.h b/cmd/sharefs/fuse-operations.h index 2150571a..fae181d1 100644 --- a/cmd/sharefs/fuse-operations.h +++ b/cmd/sharefs/fuse-operations.h @@ -6,29 +6,43 @@ #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 */ +/* owning gid presented to userspace */ +#define SHAREFS_MEDIA_RW_ID (1 << 10) - 1 +/* permission bits for directories presented to userspace */ +#define SHAREFS_PERM_DIR 0770 +/* permission bits for regular files presented to userspace */ +#define SHAREFS_PERM_REG 0660 +/* these open flags are cleared unconditionally */ +#define SHAREFS_FORBIDDEN_FLAGS O_DIRECT /* 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 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_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_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_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); +int sharefs_fsync(const char *pathname, int datasync, + struct fuse_file_info *fi); diff --git a/cmd/sharefs/testsuite/main.go b/cmd/sharefs/testsuite/main.go new file mode 100644 index 00000000..167875a1 --- /dev/null +++ b/cmd/sharefs/testsuite/main.go @@ -0,0 +1,119 @@ +//go:build testsuite + +// The sharefs test program checks cli behaviour and exercises the filesystem +// implemented by cmd/sharefs using fs_mark. +package main + +import ( + "errors" + "log" + "os" + "os/exec" + "strings" + "syscall" + + "hakurei.app/internal/testsuite" +) + +// checkBadOpts invokes cmd/sharefs with the specified options and compares +// the resulting error message. +func checkBadOpts(cred *syscall.Credential, opts, want string) { + var buf strings.Builder + buf.Grow(len(want)) + + cmd := exec.Command( + "sharefs", + "-f", + "-o", "source=/etc,"+opts, + "/mnt", + ) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGKILL, + Credential: cred, + } + cmd.Stderr = &buf + err := cmd.Run() + if err == nil { + log.Fatalf("opts=%q, unexpected success", opts) + } + if e, ok := errors.AsType[*exec.ExitError](err); !ok { + log.Fatal(err) + } else if !e.Exited() { + log.Fatal(e) + } + + if got := buf.String(); got != want { + log.Fatalf("opts=%q\n\t got:%q\n\twant:%q", opts, got, want) + } +} + +func main() { + go testsuite.ReceiveSignals() + + cred := syscall.Credential{Uid: 1000, Gid: 100} + if err := os.Mkdir("result", 0755); err != nil { + log.Fatal(err) + } + + done := make(chan struct{}) + go func() { + defer close(done) + + testsuite.MustRun( + nil, nil, + "fs_mark", + "-v", + "-d", "/sdcard/fs_mark", + "-l", "result/fs_mark.log", + ) + }() + + log.Println("checking malformed setuid/setgid representation") + checkBadOpts(&cred, "setuid=ff", "sharefs: invalid value for option setuid\n") + checkBadOpts(&cred, "setgid=ff", "sharefs: invalid value for option setgid\n") + + log.Println("checking bounds check for setuid/setgid") + checkBadOpts(&cred, "setuid=0", "sharefs: invalid value for option setuid\n") + checkBadOpts(&cred, "setgid=0", "sharefs: invalid value for option setgid\n") + checkBadOpts(&cred, "setuid=-1", "sharefs: invalid value for option setuid\n") + checkBadOpts(&cred, "setgid=-1", "sharefs: invalid value for option setgid\n") + + log.Println("checking non-root setuid/setgid") + checkBadOpts(&cred, "setuid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n") + checkBadOpts(&cred, "setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n") + checkBadOpts(&cred, "setuid=1023,setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n") + checkBadOpts(&cred, "mkdir", "sharefs: mkdir has no effect when not starting as root\n") + + log.Println("checking root without setuid/setgid") + checkBadOpts(nil, "allow_other", "sharefs: setuid and setgid must not be 0\n") + checkBadOpts(nil, "setuid=1023", "sharefs: setuid and setgid must not be 0\n") + checkBadOpts(nil, "setgid=1023", "sharefs: setuid and setgid must not be 0\n") + + log.Println("verifying mount point") + if err := os.Remove("/mnt"); err != nil { + log.Fatal(err) + } + + log.Println("checking unprivileged mount/unmount") + testsuite.MustRun(&cred, nil, "mkdir", "/tmp/sdcard", "/tmp/persistent") + testsuite.MustRun(&cred, nil, "sharefs", "-o", "source=/tmp/persistent", "/tmp/sdcard") + testsuite.MustRun(&cred, nil, "touch", "/tmp/sdcard/check") + testsuite.MustRun(&cred, nil, "umount", "/tmp/sdcard") + testsuite.MustRun(&cred, nil, "rm", "/tmp/persistent/check") + testsuite.MustRun(&cred, nil, "rmdir", "/tmp/sdcard", "/tmp/persistent") + + log.Println("waiting for fs_mark to complete") + <-done + + const backingDir = "/var/lib/sdcard" + sharefsCred := syscall.Credential{Uid: 1023, Gid: 1023} + log.Println("checking permissions") + testsuite.MustRun(&sharefsCred, nil, "touch", backingDir+"/fs_mark/.check") + testsuite.MustRun(&sharefsCred, nil, "rm", backingDir+"/fs_mark/.check") + testsuite.MustRun(&cred, nil, "rm", "-rf", "/sdcard/fs_mark") + if _, err := os.ReadDir(backingDir + "/fs_mark"); err == nil { + log.Fatal("fs_mark directory was not removed") + } else if !errors.Is(err, os.ErrNotExist) { + log.Fatal(err) + } +} diff --git a/test/sharefs/raceattr.go b/cmd/sharefs/testsuite/raceattr.go index 412cb2b3..412cb2b3 100644 --- a/test/sharefs/raceattr.go +++ b/cmd/sharefs/testsuite/raceattr.go diff --git a/container/container.go b/container/container.go index 3571d04a..6929c652 100644 --- a/container/container.go +++ b/container/container.go @@ -231,10 +231,10 @@ func (p *Container) Start() error { // map to overflow id to work around ownership checks if p.Uid < 1 { - p.Uid = OverflowUid(p.msg) + p.Uid = OverflowUID() } if p.Gid < 1 { - p.Gid = OverflowGid(p.msg) + p.Gid = OverflowGID() } if !p.RetainSession { diff --git a/container/container_test.go b/container/container_test.go index ca00fe7a..e70e23d3 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -342,8 +342,7 @@ var containerTestCases = []struct { ",upperdir="+ toHost(ctx.Value(testVal("upper")).(*check.Absolute).String())+ ",workdir="+ - toHost(ctx.Value(testVal("work")).(*check.Absolute).String())+ - ",redirect_dir=nofollow,uuid=on,userxattr"), + toHost(ctx.Value(testVal("work")).(*check.Absolute).String())), } }, 1 << 3, 1 << 14, nil, 0, std.PresetStrict}, @@ -396,8 +395,7 @@ var containerTestCases = []struct { ",lowerdir+="+ toHost(ctx.Value(testVal("lower0")).(*check.Absolute).String())+ ",lowerdir+="+ - toHost(ctx.Value(testVal("lower1")).(*check.Absolute).String())+ - ",redirect_dir=nofollow,userxattr"), + toHost(ctx.Value(testVal("lower1")).(*check.Absolute).String())), } }, 1 << 3, 1 << 14, nil, 0, std.PresetStrict}, @@ -828,6 +826,9 @@ func init() { for _, s := range []string{ "seclabel", "inode64", + "redirect_dir=nofollow", + "uuid=on", + "userxattr", } { cur.FsOptstr = strings.Replace(cur.FsOptstr, ","+s, "", 1) mnt[i].FsOptstr = strings.Replace(mnt[i].FsOptstr, ","+s, "", 1) diff --git a/container/dispatcher.go b/container/dispatcher.go index 4636a469..ba5e9c10 100644 --- a/container/dispatcher.go +++ b/container/dispatcher.go @@ -37,15 +37,15 @@ type syscallDispatcher interface { // lockOSThread provides [runtime.LockOSThread]. lockOSThread() - // setPtracer provides [SetPtracer]. + // setPtracer provides [ext.SetPtracer]. setPtracer(pid uintptr) error - // setDumpable provides [SetDumpable]. + // setDumpable provides [ext.SetDumpable]. setDumpable(dumpable uintptr) error - // setNoNewPrivs provides [SetNoNewPrivs]. + // setNoNewPrivs provides setNoNewPrivs. setNoNewPrivs() error - // lastcap provides [LastCap]. - lastcap(msg message.Msg) uintptr + // lastcap provides lastcap. + lastcap() uintptr // capset provides capset. capset(hdrp *capHeader, datap *[2]capData) error // capBoundingSetDrop provides capBoundingSetDrop. @@ -54,9 +54,9 @@ type syscallDispatcher interface { capAmbientClearAll() error // capAmbientRaise provides capAmbientRaise. capAmbientRaise(cap uintptr) error - // isatty provides [Isatty]. + // isatty provides [ext.Isatty]. isatty(fd int) bool - // receive provides [Receive]. + // receive provides [params.Receive]. receive(key string, e any, fdp *int) (closeFunc func() error, err error) // bindMount provides procPaths.bindMount. @@ -152,7 +152,7 @@ func (direct) setPtracer(pid uintptr) error { return ext.SetPtracer(pid) } func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) } 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) capBoundingSetDrop(cap uintptr) error { return capBoundingSetDrop(cap) } func (direct) capAmbientClearAll() error { return capAmbientClearAll() } diff --git a/container/dispatcher_test.go b/container/dispatcher_test.go index 242af1bf..9276a2c5 100644 --- a/container/dispatcher_test.go +++ b/container/dispatcher_test.go @@ -352,9 +352,8 @@ func (k *kstub) setDumpable(dumpable uintptr) error { } 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() - k.checkMsg(msg) return k.Expects("lastcap").Ret.(uintptr) } diff --git a/container/init.go b/container/init.go index 07fff357..03501646 100644 --- a/container/init.go +++ b/container/init.go @@ -226,7 +226,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) { } // cache sysctl before pivot_root - lastcap := k.lastcap(msg) + lc := k.lastcap() if err := k.mount(zeroString, fhs.Root, zeroString, MS_SILENT|MS_SLAVE|MS_REC, zeroString); err != nil { k.fatalf(msg, "cannot make / rslave: %v", optionalErrorUnwrap(err)) @@ -399,7 +399,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) { if err := k.capAmbientClearAll(); err != nil { k.fatalf(msg, "cannot clear the ambient capability set: %v", err) } - for i := range lastcap + 1 { + for i := range lc + 1 { if slices.Contains(keepCaps, i) { continue } diff --git a/container/mount.go b/container/mount.go index 84415fa8..978b90a9 100644 --- a/container/mount.go +++ b/container/mount.go @@ -123,7 +123,7 @@ func (p procPaths) remount(msg message.Msg, target string, flags uintptr) error { var destFd int if err := ext.IgnoringEINTR(func() (err error) { - destFd, err = p.k.open(targetFinal, O_PATH|O_CLOEXEC, 0) + destFd, err = p.k.open(targetFinal, ext.O_PATH|O_CLOEXEC, 0) return }); err != nil { return &os.PathError{Op: "open", Path: targetFinal, Err: err} diff --git a/container/seccomp/libseccomp-helper.c b/container/seccomp/libseccomp-helper.c index de8abc8d..7c640bac 100644 --- a/container/seccomp/libseccomp-helper.c +++ b/container/seccomp/libseccomp-helper.c @@ -9,130 +9,142 @@ #define LEN(arr) (sizeof(arr) / sizeof((arr)[0])) -int32_t hakurei_scmp_make_filter( - int *ret_p, uintptr_t allocate_p, - uint32_t arch, uint32_t multiarch, - struct hakurei_syscall_rule *rules, - size_t rules_sz, hakurei_export_flag flags) { - int i; - int last_allowed_family; - int disallowed; - struct hakurei_syscall_rule *rule; - void *buf; - size_t len = 0; +int32_t hakurei_scmp_make_filter(int *ret_p, uintptr_t allocate_p, + uint32_t arch, uint32_t multiarch, + struct hakurei_syscall_rule *rules, + size_t rules_sz, hakurei_export_flag flags) +{ + int i; + int last_allowed_family; + int disallowed; + struct hakurei_syscall_rule *rule; + void *buf; + size_t len = 0; - int32_t res = 0; /* refer to resPrefix for message */ + int32_t res = 0; /* refer to resPrefix for message */ - /* Blocklist all but unix, inet, inet6 and netlink */ - struct { - int family; - hakurei_export_flag flags_mask; - } socket_family_allowlist[] = { - /* NOTE: Keep in numerical order */ - {AF_UNSPEC, 0}, - {AF_LOCAL, 0}, - {AF_INET, 0}, - {AF_INET6, 0}, - {AF_NETLINK, 0}, - {AF_CAN, HAKUREI_EXPORT_CAN}, - {AF_BLUETOOTH, HAKUREI_EXPORT_BLUETOOTH}, - }; + /* Blocklist all but unix, inet, inet6 and netlink */ + struct { + int family; + hakurei_export_flag flags_mask; + } socket_family_allowlist[] = { + /* NOTE: Keep in numerical order */ + { AF_UNSPEC, 0 }, + { AF_LOCAL, 0 }, + { AF_INET, 0 }, + { AF_INET6, 0 }, + { AF_NETLINK, 0 }, + { AF_CAN, HAKUREI_EXPORT_CAN }, + { AF_BLUETOOTH, HAKUREI_EXPORT_BLUETOOTH }, + }; - scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW); - if (ctx == NULL) { - res = 1; - goto out; - } else - errno = 0; + scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW); + if (ctx == NULL) { + res = 1; + goto out; + } else + errno = 0; - /* We only really need to handle arches on multiarch systems. + /* We only really need to handle arches on multiarch systems. * If only one arch is supported the default is fine */ - if (arch != 0) { - /* This *adds* the target arch, instead of replacing the + if (arch != 0) { + /* This *adds* the target arch, instead of replacing the * native one. This is not ideal, because we'd like to only * allow the target arch, but we can't really disallow the * native arch at this point, because then bubblewrap * couldn't continue running. */ - *ret_p = seccomp_arch_add(ctx, arch); - if (*ret_p < 0 && *ret_p != -EEXIST) { - res = 2; - goto out; - } + *ret_p = seccomp_arch_add(ctx, arch); + if (*ret_p < 0 && *ret_p != -EEXIST) { + res = 2; + goto out; + } - if (flags & HAKUREI_EXPORT_MULTIARCH && multiarch != 0) { - *ret_p = seccomp_arch_add(ctx, multiarch); - if (*ret_p < 0 && *ret_p != -EEXIST) { - res = 3; - goto out; - } - } - } + if (flags & HAKUREI_EXPORT_MULTIARCH && multiarch != 0) { + *ret_p = seccomp_arch_add(ctx, multiarch); + if (*ret_p < 0 && *ret_p != -EEXIST) { + res = 3; + goto out; + } + } + } - for (i = 0; i < rules_sz; i++) { - rule = &rules[i]; - assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS); + for (i = 0; i < rules_sz; i++) { + rule = &rules[i]; + assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS); - if (rule->arg) - *ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno), rule->syscall, 1, *rule->arg); - else - *ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno), rule->syscall, 0); + if (rule->arg) + *ret_p = seccomp_rule_add(ctx, + SCMP_ACT_ERRNO(rule->m_errno), + rule->syscall, 1, *rule->arg); + else + *ret_p = seccomp_rule_add(ctx, + SCMP_ACT_ERRNO(rule->m_errno), + rule->syscall, 0); - if (*ret_p == -EFAULT) { - res = 4; - goto out; - } else if (*ret_p < 0) { - res = 5; - goto out; - } - } + if (*ret_p == -EFAULT) { + res = 4; + goto out; + } else if (*ret_p < 0) { + res = 5; + goto out; + } + } - /* Socket filtering doesn't work on e.g. i386, so ignore failures here + /* Socket filtering doesn't work on e.g. i386, so ignore failures here * However, we need to user seccomp_rule_add_exact to avoid libseccomp doing * something else: https://github.com/seccomp/libseccomp/issues/8 */ - last_allowed_family = -1; - for (i = 0; i < LEN(socket_family_allowlist); i++) { - if (socket_family_allowlist[i].flags_mask != 0 && - (socket_family_allowlist[i].flags_mask & flags) != socket_family_allowlist[i].flags_mask) - continue; + last_allowed_family = -1; + for (i = 0; i < LEN(socket_family_allowlist); i++) { + if (socket_family_allowlist[i].flags_mask != 0 && + (socket_family_allowlist[i].flags_mask & flags) != + socket_family_allowlist[i].flags_mask) + continue; - for (disallowed = last_allowed_family + 1; disallowed < socket_family_allowlist[i].family; disallowed++) { - /* Blocklist the in-between valid families */ - seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_EQ, disallowed)); - } - last_allowed_family = socket_family_allowlist[i].family; - } - /* Blocklist the rest */ - seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1)); + for (disallowed = last_allowed_family + 1; + disallowed < socket_family_allowlist[i].family; + disallowed++) { + /* Blocklist the in-between valid families */ + seccomp_rule_add_exact( + ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), + SCMP_SYS(socket), 1, + SCMP_A0(SCMP_CMP_EQ, disallowed)); + } + last_allowed_family = socket_family_allowlist[i].family; + } + /* Blocklist the rest */ + seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), + SCMP_SYS(socket), 1, + SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1)); - if (allocate_p == 0) { - *ret_p = seccomp_load(ctx); - if (*ret_p != 0) { - res = 7; - goto out; - } - } else { - *ret_p = seccomp_export_bpf_mem(ctx, NULL, &len); - if (*ret_p != 0) { - res = 6; - goto out; - } + if (allocate_p == 0) { + *ret_p = seccomp_load(ctx); + if (*ret_p != 0) { + res = 7; + goto out; + } + } else { + *ret_p = seccomp_export_bpf_mem(ctx, NULL, &len); + if (*ret_p != 0) { + res = 6; + goto out; + } - buf = hakurei_scmp_allocate(allocate_p, len); - if (buf == NULL) { - res = 4; - goto out; - } + buf = hakurei_scmp_allocate(allocate_p, len); + if (buf == NULL) { + res = 4; + goto out; + } - *ret_p = seccomp_export_bpf_mem(ctx, buf, &len); - if (*ret_p != 0) { - res = 6; - goto out; - } - } + *ret_p = seccomp_export_bpf_mem(ctx, buf, &len); + if (*ret_p != 0) { + res = 6; + goto out; + } + } out: - if (ctx) - seccomp_release(ctx); + if (ctx) + seccomp_release(ctx); - return res; + return res; } diff --git a/container/seccomp/libseccomp-helper.h b/container/seccomp/libseccomp-helper.h index 655110e4..29d5d5de 100644 --- a/container/seccomp/libseccomp-helper.h +++ b/container/seccomp/libseccomp-helper.h @@ -2,25 +2,24 @@ #include <stdint.h> #if (SCMP_VER_MAJOR < 2) || (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR < 5) || \ - (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR == 5 && SCMP_VER_MICRO < 1) + (SCMP_VER_MAJOR == 2 && SCMP_VER_MINOR == 5 && SCMP_VER_MICRO < 1) #error This package requires libseccomp >= v2.5.1 #endif typedef enum { - HAKUREI_EXPORT_MULTIARCH = 1 << 0, - HAKUREI_EXPORT_CAN = 1 << 1, - HAKUREI_EXPORT_BLUETOOTH = 1 << 2, + HAKUREI_EXPORT_MULTIARCH = 1 << 0, + HAKUREI_EXPORT_CAN = 1 << 1, + HAKUREI_EXPORT_BLUETOOTH = 1 << 2, } hakurei_export_flag; struct hakurei_syscall_rule { - int syscall; - int m_errno; - struct scmp_arg_cmp *arg; + int syscall; + int m_errno; + struct scmp_arg_cmp *arg; }; extern void *hakurei_scmp_allocate(uintptr_t f, size_t len); -int32_t hakurei_scmp_make_filter( - int *ret_p, uintptr_t allocate_p, - uint32_t arch, uint32_t multiarch, - struct hakurei_syscall_rule *rules, - size_t rules_sz, hakurei_export_flag flags); +int32_t hakurei_scmp_make_filter(int *ret_p, uintptr_t allocate_p, + uint32_t arch, uint32_t multiarch, + struct hakurei_syscall_rule *rules, + size_t rules_sz, hakurei_export_flag flags); diff --git a/container/seccomp/presets.go b/container/seccomp/presets.go index 4b76af61..252bf9f1 100644 --- a/container/seccomp/presets.go +++ b/container/seccomp/presets.go @@ -3,7 +3,7 @@ package seccomp /* flatpak commit 4c3bf179e2e4a2a298cd1db1d045adaf3f564532 */ import ( - . "syscall" + "syscall" . "hakurei.app/container/std" . "hakurei.app/ext" @@ -69,121 +69,124 @@ func Preset(presets FilterPreset, flags ExportFlag) (rules []NativeRule) { var ( presetCommon = []NativeRule{ /* Block dmesg */ - {Syscall: SNR_SYSLOG, Errno: ScmpErrno(EPERM), Arg: nil}, + {Syscall: SNR_SYSLOG, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, /* Useless old syscall */ - {Syscall: SNR_USELIB, Errno: ScmpErrno(EPERM), Arg: nil}, + {Syscall: SNR_USELIB, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, /* Don't allow disabling accounting */ - {Syscall: SNR_ACCT, Errno: ScmpErrno(EPERM), Arg: nil}, + {Syscall: SNR_ACCT, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, /* Don't allow reading current quota use */ - {Syscall: SNR_QUOTACTL, Errno: ScmpErrno(EPERM), Arg: nil}, + {Syscall: SNR_QUOTACTL, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, /* Don't allow access to the kernel keyring */ - {Syscall: SNR_ADD_KEY, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_KEYCTL, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_REQUEST_KEY, Errno: ScmpErrno(EPERM), Arg: nil}, + {Syscall: SNR_ADD_KEY, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_KEYCTL, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_REQUEST_KEY, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, /* Scary VM/NUMA ops */ - {Syscall: SNR_MOVE_PAGES, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_MBIND, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_GET_MEMPOLICY, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_SET_MEMPOLICY, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_MIGRATE_PAGES, Errno: ScmpErrno(EPERM), Arg: nil}, + {Syscall: SNR_MOVE_PAGES, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_MBIND, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_GET_MEMPOLICY, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_SET_MEMPOLICY, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_MIGRATE_PAGES, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, } /* hakurei: project-specific extensions */ presetCommonExt = []NativeRule{ /* system calls for changing the system clock */ - {Syscall: SNR_ADJTIMEX, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_CLOCK_ADJTIME, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_CLOCK_ADJTIME64, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_CLOCK_SETTIME, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_CLOCK_SETTIME64, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_SETTIMEOFDAY, Errno: ScmpErrno(EPERM), Arg: nil}, + {Syscall: SNR_ADJTIMEX, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_CLOCK_ADJTIME, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_CLOCK_ADJTIME64, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_CLOCK_SETTIME, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_CLOCK_SETTIME64, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_SETTIMEOFDAY, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, /* loading and unloading of kernel modules */ - {Syscall: SNR_DELETE_MODULE, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_FINIT_MODULE, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_INIT_MODULE, Errno: ScmpErrno(EPERM), Arg: nil}, + {Syscall: SNR_DELETE_MODULE, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_FINIT_MODULE, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_INIT_MODULE, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, /* system calls for rebooting and reboot preparation */ - {Syscall: SNR_KEXEC_FILE_LOAD, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_KEXEC_LOAD, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_REBOOT, Errno: ScmpErrno(EPERM), Arg: nil}, + {Syscall: SNR_KEXEC_FILE_LOAD, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_KEXEC_LOAD, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_REBOOT, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, /* system calls for enabling/disabling swap devices */ - {Syscall: SNR_SWAPOFF, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_SWAPON, Errno: ScmpErrno(EPERM), Arg: nil}, + {Syscall: SNR_SWAPOFF, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_SWAPON, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, } presetNamespace = []NativeRule{ /* Don't allow subnamespace setups: */ - {Syscall: SNR_UNSHARE, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_SETNS, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_MOUNT, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_UMOUNT, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_UMOUNT2, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_PIVOT_ROOT, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_CHROOT, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_CLONE, Errno: ScmpErrno(EPERM), - Arg: &ScmpArgCmp{Arg: cloneArg, Op: SCMP_CMP_MASKED_EQ, DatumA: CLONE_NEWUSER, DatumB: CLONE_NEWUSER}}, + {Syscall: SNR_UNSHARE, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_SETNS, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_MOUNT, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_UMOUNT, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_UMOUNT2, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_PIVOT_ROOT, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_CHROOT, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_CLONE, Errno: ScmpErrno(syscall.EPERM), + Arg: &ScmpArgCmp{Arg: cloneArg, Op: SCMP_CMP_MASKED_EQ, + DatumA: syscall.CLONE_NEWUSER, DatumB: syscall.CLONE_NEWUSER}}, /* seccomp can't look into clone3()'s struct clone_args to check whether * the flags are OK, so we have no choice but to block clone3(). - * Return ENOSYS so user-space will fall back to clone(). + * Return syscall.ENOSYS so user-space will fall back to clone(). * (CVE-2021-41133; see also https://github.com/moby/moby/commit/9f6b562d) */ - {Syscall: SNR_CLONE3, Errno: ScmpErrno(ENOSYS), Arg: nil}, + {Syscall: SNR_CLONE3, Errno: ScmpErrno(syscall.ENOSYS), Arg: nil}, /* New mount manipulation APIs can also change our VFS. There's no * legitimate reason to do these in the sandbox, so block all of them * rather than thinking about which ones might be dangerous. * (CVE-2021-41133) */ - {Syscall: SNR_OPEN_TREE, Errno: ScmpErrno(ENOSYS), Arg: nil}, - {Syscall: SNR_MOVE_MOUNT, Errno: ScmpErrno(ENOSYS), Arg: nil}, - {Syscall: SNR_FSOPEN, Errno: ScmpErrno(ENOSYS), Arg: nil}, - {Syscall: SNR_FSCONFIG, Errno: ScmpErrno(ENOSYS), Arg: nil}, - {Syscall: SNR_FSMOUNT, Errno: ScmpErrno(ENOSYS), Arg: nil}, - {Syscall: SNR_FSPICK, Errno: ScmpErrno(ENOSYS), Arg: nil}, - {Syscall: SNR_MOUNT_SETATTR, Errno: ScmpErrno(ENOSYS), Arg: nil}, + {Syscall: SNR_OPEN_TREE, Errno: ScmpErrno(syscall.ENOSYS), Arg: nil}, + {Syscall: SNR_MOVE_MOUNT, Errno: ScmpErrno(syscall.ENOSYS), Arg: nil}, + {Syscall: SNR_FSOPEN, Errno: ScmpErrno(syscall.ENOSYS), Arg: nil}, + {Syscall: SNR_FSCONFIG, Errno: ScmpErrno(syscall.ENOSYS), Arg: nil}, + {Syscall: SNR_FSMOUNT, Errno: ScmpErrno(syscall.ENOSYS), Arg: nil}, + {Syscall: SNR_FSPICK, Errno: ScmpErrno(syscall.ENOSYS), Arg: nil}, + {Syscall: SNR_MOUNT_SETATTR, Errno: ScmpErrno(syscall.ENOSYS), Arg: nil}, } /* hakurei: project-specific extensions */ presetNamespaceExt = []NativeRule{ /* changing file ownership */ - {Syscall: SNR_CHOWN, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_CHOWN32, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_FCHOWN, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_FCHOWN32, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_FCHOWNAT, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_LCHOWN, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_LCHOWN32, Errno: ScmpErrno(EPERM), Arg: nil}, + {Syscall: SNR_CHOWN, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_CHOWN32, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_FCHOWN, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_FCHOWN32, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_FCHOWNAT, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_LCHOWN, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_LCHOWN32, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, /* system calls for changing user ID and group ID credentials */ - {Syscall: SNR_SETGID, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_SETGID32, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_SETGROUPS, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_SETGROUPS32, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_SETREGID, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_SETREGID32, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_SETRESGID, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_SETRESGID32, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_SETRESUID, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_SETRESUID32, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_SETREUID, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_SETREUID32, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_SETUID, Errno: ScmpErrno(EPERM), Arg: nil}, - {Syscall: SNR_SETUID32, Errno: ScmpErrno(EPERM), Arg: nil}, + {Syscall: SNR_SETGID, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_SETGID32, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_SETGROUPS, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_SETGROUPS32, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_SETREGID, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_SETREGID32, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_SETRESGID, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_SETRESGID32, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_SETRESUID, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_SETRESUID32, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_SETREUID, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_SETREUID32, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_SETUID, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, + {Syscall: SNR_SETUID32, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, } presetTTY = []NativeRule{ /* Don't allow faking input to the controlling tty (CVE-2017-5226) */ - {Syscall: SNR_IOCTL, Errno: ScmpErrno(EPERM), - Arg: &ScmpArgCmp{Arg: 1, Op: SCMP_CMP_MASKED_EQ, DatumA: 0xFFFFFFFF, DatumB: TIOCSTI}}, + {Syscall: SNR_IOCTL, Errno: ScmpErrno(syscall.EPERM), + Arg: &ScmpArgCmp{Arg: 1, Op: SCMP_CMP_MASKED_EQ, + DatumA: 0xFFFFFFFF, DatumB: syscall.TIOCSTI}}, /* In the unlikely event that the controlling tty is a Linux virtual * console (/dev/tty2 or similar), copy/paste operations have an effect * similar to TIOCSTI (CVE-2023-28100) */ - {Syscall: SNR_IOCTL, Errno: ScmpErrno(EPERM), - Arg: &ScmpArgCmp{Arg: 1, Op: SCMP_CMP_MASKED_EQ, DatumA: 0xFFFFFFFF, DatumB: TIOCLINUX}}, + {Syscall: SNR_IOCTL, Errno: ScmpErrno(syscall.EPERM), + Arg: &ScmpArgCmp{Arg: 1, Op: SCMP_CMP_MASKED_EQ, + DatumA: 0xFFFFFFFF, DatumB: syscall.TIOCLINUX}}, } presetEmu = []NativeRule{ @@ -191,15 +194,15 @@ var ( * so it's disabled as a hardening measure. * However, it is required to run old 16-bit applications * as well as some Wine patches, so it's allowed in multiarch. */ - {Syscall: SNR_MODIFY_LDT, Errno: ScmpErrno(EPERM), Arg: nil}, + {Syscall: SNR_MODIFY_LDT, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, } /* hakurei: project-specific extensions */ presetEmuExt = []NativeRule{ - {Syscall: SNR_SUBPAGE_PROT, Errno: ScmpErrno(ENOSYS), Arg: nil}, - {Syscall: SNR_SWITCH_ENDIAN, Errno: ScmpErrno(ENOSYS), Arg: nil}, - {Syscall: SNR_VM86, Errno: ScmpErrno(ENOSYS), Arg: nil}, - {Syscall: SNR_VM86OLD, Errno: ScmpErrno(ENOSYS), Arg: nil}, + {Syscall: SNR_SUBPAGE_PROT, Errno: ScmpErrno(syscall.ENOSYS), Arg: nil}, + {Syscall: SNR_SWITCH_ENDIAN, Errno: ScmpErrno(syscall.ENOSYS), Arg: nil}, + {Syscall: SNR_VM86, Errno: ScmpErrno(syscall.ENOSYS), Arg: nil}, + {Syscall: SNR_VM86OLD, Errno: ScmpErrno(syscall.ENOSYS), Arg: nil}, } ) @@ -207,11 +210,11 @@ func presetDevel(allowedPersonality ScmpDatum) []NativeRule { return []NativeRule{ /* Profiling operations; we expect these to be done by tools from outside * the sandbox. In particular perf has been the source of many CVEs. */ - {Syscall: SNR_PERF_EVENT_OPEN, Errno: ScmpErrno(EPERM), Arg: nil}, + {Syscall: SNR_PERF_EVENT_OPEN, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, /* Don't allow you to switch to bsd emulation or whatnot */ - {Syscall: SNR_PERSONALITY, Errno: ScmpErrno(EPERM), + {Syscall: SNR_PERSONALITY, Errno: ScmpErrno(syscall.EPERM), Arg: &ScmpArgCmp{Arg: 0, Op: SCMP_CMP_NE, DatumA: allowedPersonality}}, - {Syscall: SNR_PTRACE, Errno: ScmpErrno(EPERM), Arg: nil}, + {Syscall: SNR_PTRACE, Errno: ScmpErrno(syscall.EPERM), Arg: nil}, } } diff --git a/container/syscall.go b/container/syscall.go index e008ecea..43e49bea 100644 --- a/container/syscall.go +++ b/container/syscall.go @@ -9,7 +9,7 @@ import ( // setNoNewPrivs sets the calling thread's no_new_privs attribute. func setNoNewPrivs() error { - return ext.Prctl(PR_SET_NO_NEW_PRIVS, 1, 0) + return ext.Prctl(ext.PR_SET_NO_NEW_PRIVS, 1, 0) } // schedParam is equivalent to struct sched_param from include/linux/sched.h. diff --git a/container/syscall_386.go b/container/syscall_386.go deleted file mode 100644 index 4b8e2df3..00000000 --- a/container/syscall_386.go +++ /dev/null @@ -1,7 +0,0 @@ -package container - -const ( - O_PATH = 0x200000 - - PR_SET_NO_NEW_PRIVS = 0x26 -) diff --git a/container/syscall_amd64.go b/container/syscall_amd64.go deleted file mode 100644 index 4b8e2df3..00000000 --- a/container/syscall_amd64.go +++ /dev/null @@ -1,7 +0,0 @@ -package container - -const ( - O_PATH = 0x200000 - - PR_SET_NO_NEW_PRIVS = 0x26 -) diff --git a/container/sysctl.go b/container/sysctl.go index e4b7af23..2cf47258 100644 --- a/container/sysctl.go +++ b/container/sysctl.go @@ -2,14 +2,32 @@ package container import ( "bytes" + "fmt" "os" "strconv" "sync" "hakurei.app/fhs" - "hakurei.app/message" ) +// mustSysctlInt reads a sysctl path via proc, interprets it as a decimal +// representation of an integer, and terminates the program if the read was +// unsuccessful or the response is invalid. +func mustSysctlInt(path string) (v int) { + p, err := os.ReadFile(fhs.ProcSys + path) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + if v, err = strconv.Atoi(string(bytes.TrimSpace(p))); err != nil { + fmt.Printf("invalid sysctl response %q\n", string(p)) + os.Exit(1) + } + return +} + +// cached sysctl responses for populateSysctl var ( kernelOverflowuid int kernelOverflowgid int @@ -18,34 +36,31 @@ var ( sysctlOnce sync.Once ) -const ( - kernelOverflowuidPath = fhs.ProcSys + "kernel/overflowuid" - kernelOverflowgidPath = fhs.ProcSys + "kernel/overflowgid" - kernelCapLastCapPath = fhs.ProcSys + "kernel/cap_last_cap" -) - -func mustReadSysctl(msg message.Msg) { +// populateSysctl populates cached sysctl responses guarded by sysctlOnce. +func populateSysctl() { sysctlOnce.Do(func() { - if v, err := os.ReadFile(kernelOverflowuidPath); err != nil { - msg.GetLogger().Fatalf("cannot read %q: %v", kernelOverflowuidPath, err) - } else if kernelOverflowuid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil { - msg.GetLogger().Fatalf("cannot interpret %q: %v", kernelOverflowuidPath, err) - } + kernelOverflowuid = mustSysctlInt("kernel/overflowuid") + kernelOverflowgid = mustSysctlInt("kernel/overflowgid") + kernelCapLastCap = mustSysctlInt("kernel/cap_last_cap") + }) +} - if v, err := os.ReadFile(kernelOverflowgidPath); err != nil { - msg.GetLogger().Fatalf("cannot read %q: %v", kernelOverflowgidPath, err) - } else if kernelOverflowgid, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil { - msg.GetLogger().Fatalf("cannot interpret %q: %v", kernelOverflowgidPath, err) - } +// OverflowUID returns the fixed uid returned by legacy 16-bit UID system calls. +// This value is also returned for unmapped UIDs in a user namespace. +func OverflowUID() int { + populateSysctl() + return kernelOverflowuid +} - if v, err := os.ReadFile(kernelCapLastCapPath); err != nil { - msg.GetLogger().Fatalf("cannot read %q: %v", kernelCapLastCapPath, err) - } else if kernelCapLastCap, err = strconv.Atoi(string(bytes.TrimSpace(v))); err != nil { - msg.GetLogger().Fatalf("cannot interpret %q: %v", kernelCapLastCapPath, err) - } - }) +// OverflowGID is like [OverflowUID], but for GID instead. +func OverflowGID() int { + populateSysctl() + return kernelOverflowgid } -func OverflowUid(msg message.Msg) int { mustReadSysctl(msg); return kernelOverflowuid } -func OverflowGid(msg message.Msg) int { mustReadSysctl(msg); return kernelOverflowgid } -func LastCap(msg message.Msg) uintptr { mustReadSysctl(msg); return uintptr(kernelCapLastCap) } +// lastcap returns the highest valid capability of the running kernel. Exports +// CAP_LAST_CAP from the kernel. +func lastcap() uintptr { + populateSysctl() + return uintptr(kernelCapLastCap) +} diff --git a/container/sysctl_deprecated.go b/container/sysctl_deprecated.go new file mode 100644 index 00000000..3c76be2c --- /dev/null +++ b/container/sysctl_deprecated.go @@ -0,0 +1,19 @@ +package container + +import "hakurei.app/message" + +// OverflowUid is the old function signature of [OverflowUID]. +// +// Deprecated: This function will be removed in 0.5. +func OverflowUid(_ message.Msg) int { return OverflowUID() } + +// OverflowGid is the old function signature of [OverflowGID]. +// +// Deprecated: This function will be removed in 0.5. +func OverflowGid(_ message.Msg) int { return OverflowGID() } + +// LastCap returns the highest valid capability of the running kernel. Exports +// CAP_LAST_CAP from the kernel. +// +// Deprecated: This function will be unexported in 0.5. +func LastCap(_ message.Msg) uintptr { return lastcap() } diff --git a/ext/mksysnum_linux.pl b/ext/mksysnum_linux.pl index 1921e7b9..1b381b28 100755 --- a/ext/mksysnum_linux.pl +++ b/ext/mksysnum_linux.pl @@ -7,13 +7,6 @@ use strict; use POSIX (); my $command = "mksysnum_linux.pl ". join(' ', @ARGV); -my $uname_arch = (POSIX::uname)[4]; -my %syscall_cutoff_arch = ( - "x86" => 340, - "x86_64" => 302, - "aarch64" => 281, - "riscv64" => 281, -); print <<EOF; // $command @@ -21,8 +14,6 @@ print <<EOF; package ext -import . "syscall" - var syscallNum = map[string]SyscallNum{ EOF @@ -38,7 +29,7 @@ sub fmt { } (my $name_upper = $name) =~ y/a-z/A-Z/; $num = $num + $offset; - if($num > $syscall_cutoff_arch{$uname_arch} && $state == 0){ # not wired in Go standard library + if($state == 0){ # not wired in Go standard library print " SYS_$name_upper = $num\n"; } elsif($state == -1){ @@ -55,7 +46,7 @@ sub fmt { GENERATE: my $prev; -open(GCC, "gcc -E -dD $ARGV[0] |") || die "can't run gcc"; +open(GCC, "clang -E -dD $ARGV[0] |") || die "cannot run clang"; while(<GCC>){ if(/^#define __NR_Linux\s+([0-9]+)/){ # mips/mips64: extract offset diff --git a/ext/syscall_extra.go b/ext/syscall_extra.go new file mode 100644 index 00000000..89adf1f3 --- /dev/null +++ b/ext/syscall_extra.go @@ -0,0 +1,9 @@ +//go:build !(arm64 || riscv64 || mips || mipsle || mips64 || mips64le || ppc64 || ppc64le || s390x || loong64) + +package ext + +const ( + O_PATH = 0x200000 + + PR_SET_NO_NEW_PRIVS = 0x26 +) diff --git a/ext/syscall_extra_linux_arm64.go b/ext/syscall_extra_linux_arm64.go index 977c3cf3..91f98686 100644 --- a/ext/syscall_extra_linux_arm64.go +++ b/ext/syscall_extra_linux_arm64.go @@ -1,11 +1,5 @@ package ext -import "syscall" - -const ( - SYS_NEWFSTATAT = syscall.SYS_FSTATAT -) - var syscallNumExtra = map[string]SyscallNum{ "uselib": SNR_USELIB, "clock_adjtime64": SNR_CLOCK_ADJTIME64, diff --git a/ext/syscall_extra_linux_riscv64.go b/ext/syscall_extra_linux_riscv64.go index 977c3cf3..91f98686 100644 --- a/ext/syscall_extra_linux_riscv64.go +++ b/ext/syscall_extra_linux_riscv64.go @@ -1,11 +1,5 @@ package ext -import "syscall" - -const ( - SYS_NEWFSTATAT = syscall.SYS_FSTATAT -) - var syscallNumExtra = map[string]SyscallNum{ "uselib": SNR_USELIB, "clock_adjtime64": SNR_CLOCK_ADJTIME64, diff --git a/ext/syscall_linux_386.go b/ext/syscall_linux_386.go index c306e5fe..91e87eae 100644 --- a/ext/syscall_linux_386.go +++ b/ext/syscall_linux_386.go @@ -3,8 +3,6 @@ package ext -import . "syscall" - var syscallNum = map[string]SyscallNum{ "restart_syscall": SNR_RESTART_SYSCALL, "exit": SNR_EXIT, @@ -458,9 +456,350 @@ var syscallNum = map[string]SyscallNum{ "lsm_set_self_attr": SNR_LSM_SET_SELF_ATTR, "lsm_list_modules": SNR_LSM_LIST_MODULES, "mseal": SNR_MSEAL, + "setxattrat": SNR_SETXATTRAT, + "getxattrat": SNR_GETXATTRAT, + "listxattrat": SNR_LISTXATTRAT, + "removexattrat": SNR_REMOVEXATTRAT, } const ( + SYS_RESTART_SYSCALL = 0 + SYS_EXIT = 1 + SYS_FORK = 2 + SYS_READ = 3 + SYS_WRITE = 4 + SYS_OPEN = 5 + SYS_CLOSE = 6 + SYS_WAITPID = 7 + SYS_CREAT = 8 + SYS_LINK = 9 + SYS_UNLINK = 10 + SYS_EXECVE = 11 + SYS_CHDIR = 12 + SYS_TIME = 13 + SYS_MKNOD = 14 + SYS_CHMOD = 15 + SYS_LCHOWN = 16 + SYS_BREAK = 17 + SYS_OLDSTAT = 18 + SYS_LSEEK = 19 + SYS_GETPID = 20 + SYS_MOUNT = 21 + SYS_UMOUNT = 22 + SYS_SETUID = 23 + SYS_GETUID = 24 + SYS_STIME = 25 + SYS_PTRACE = 26 + SYS_ALARM = 27 + SYS_OLDFSTAT = 28 + SYS_PAUSE = 29 + SYS_UTIME = 30 + SYS_STTY = 31 + SYS_GTTY = 32 + SYS_ACCESS = 33 + SYS_NICE = 34 + SYS_FTIME = 35 + SYS_SYNC = 36 + SYS_KILL = 37 + SYS_RENAME = 38 + SYS_MKDIR = 39 + SYS_RMDIR = 40 + SYS_DUP = 41 + SYS_PIPE = 42 + SYS_TIMES = 43 + SYS_PROF = 44 + SYS_BRK = 45 + SYS_SETGID = 46 + SYS_GETGID = 47 + SYS_SIGNAL = 48 + SYS_GETEUID = 49 + SYS_GETEGID = 50 + SYS_ACCT = 51 + SYS_UMOUNT2 = 52 + SYS_LOCK = 53 + SYS_IOCTL = 54 + SYS_FCNTL = 55 + SYS_MPX = 56 + SYS_SETPGID = 57 + SYS_ULIMIT = 58 + SYS_OLDOLDUNAME = 59 + SYS_UMASK = 60 + SYS_CHROOT = 61 + SYS_USTAT = 62 + SYS_DUP2 = 63 + SYS_GETPPID = 64 + SYS_GETPGRP = 65 + SYS_SETSID = 66 + SYS_SIGACTION = 67 + SYS_SGETMASK = 68 + SYS_SSETMASK = 69 + SYS_SETREUID = 70 + SYS_SETREGID = 71 + SYS_SIGSUSPEND = 72 + SYS_SIGPENDING = 73 + SYS_SETHOSTNAME = 74 + SYS_SETRLIMIT = 75 + SYS_GETRLIMIT = 76 + SYS_GETRUSAGE = 77 + SYS_GETTIMEOFDAY = 78 + SYS_SETTIMEOFDAY = 79 + SYS_GETGROUPS = 80 + SYS_SETGROUPS = 81 + SYS_SELECT = 82 + SYS_SYMLINK = 83 + SYS_OLDLSTAT = 84 + SYS_READLINK = 85 + SYS_USELIB = 86 + SYS_SWAPON = 87 + SYS_REBOOT = 88 + SYS_READDIR = 89 + SYS_MMAP = 90 + SYS_MUNMAP = 91 + SYS_TRUNCATE = 92 + SYS_FTRUNCATE = 93 + SYS_FCHMOD = 94 + SYS_FCHOWN = 95 + SYS_GETPRIORITY = 96 + SYS_SETPRIORITY = 97 + SYS_PROFIL = 98 + SYS_STATFS = 99 + SYS_FSTATFS = 100 + SYS_IOPERM = 101 + SYS_SOCKETCALL = 102 + SYS_SYSLOG = 103 + SYS_SETITIMER = 104 + SYS_GETITIMER = 105 + SYS_STAT = 106 + SYS_LSTAT = 107 + SYS_FSTAT = 108 + SYS_OLDUNAME = 109 + SYS_IOPL = 110 + SYS_VHANGUP = 111 + SYS_IDLE = 112 + SYS_VM86OLD = 113 + SYS_WAIT4 = 114 + SYS_SWAPOFF = 115 + SYS_SYSINFO = 116 + SYS_IPC = 117 + SYS_FSYNC = 118 + SYS_SIGRETURN = 119 + SYS_CLONE = 120 + SYS_SETDOMAINNAME = 121 + SYS_UNAME = 122 + SYS_MODIFY_LDT = 123 + SYS_ADJTIMEX = 124 + SYS_MPROTECT = 125 + SYS_SIGPROCMASK = 126 + SYS_CREATE_MODULE = 127 + SYS_INIT_MODULE = 128 + SYS_DELETE_MODULE = 129 + SYS_GET_KERNEL_SYMS = 130 + SYS_QUOTACTL = 131 + SYS_GETPGID = 132 + SYS_FCHDIR = 133 + SYS_BDFLUSH = 134 + SYS_SYSFS = 135 + SYS_PERSONALITY = 136 + SYS_AFS_SYSCALL = 137 + SYS_SETFSUID = 138 + SYS_SETFSGID = 139 + SYS__LLSEEK = 140 + SYS_GETDENTS = 141 + SYS__NEWSELECT = 142 + SYS_FLOCK = 143 + SYS_MSYNC = 144 + SYS_READV = 145 + SYS_WRITEV = 146 + SYS_GETSID = 147 + SYS_FDATASYNC = 148 + SYS__SYSCTL = 149 + SYS_MLOCK = 150 + SYS_MUNLOCK = 151 + SYS_MLOCKALL = 152 + SYS_MUNLOCKALL = 153 + SYS_SCHED_SETPARAM = 154 + SYS_SCHED_GETPARAM = 155 + SYS_SCHED_SETSCHEDULER = 156 + SYS_SCHED_GETSCHEDULER = 157 + SYS_SCHED_YIELD = 158 + SYS_SCHED_GET_PRIORITY_MAX = 159 + SYS_SCHED_GET_PRIORITY_MIN = 160 + SYS_SCHED_RR_GET_INTERVAL = 161 + SYS_NANOSLEEP = 162 + SYS_MREMAP = 163 + SYS_SETRESUID = 164 + SYS_GETRESUID = 165 + SYS_VM86 = 166 + SYS_QUERY_MODULE = 167 + SYS_POLL = 168 + SYS_NFSSERVCTL = 169 + SYS_SETRESGID = 170 + SYS_GETRESGID = 171 + SYS_PRCTL = 172 + SYS_RT_SIGRETURN = 173 + SYS_RT_SIGACTION = 174 + SYS_RT_SIGPROCMASK = 175 + SYS_RT_SIGPENDING = 176 + SYS_RT_SIGTIMEDWAIT = 177 + SYS_RT_SIGQUEUEINFO = 178 + SYS_RT_SIGSUSPEND = 179 + SYS_PREAD64 = 180 + SYS_PWRITE64 = 181 + SYS_CHOWN = 182 + SYS_GETCWD = 183 + SYS_CAPGET = 184 + SYS_CAPSET = 185 + SYS_SIGALTSTACK = 186 + SYS_SENDFILE = 187 + SYS_GETPMSG = 188 + SYS_PUTPMSG = 189 + SYS_VFORK = 190 + SYS_UGETRLIMIT = 191 + SYS_MMAP2 = 192 + SYS_TRUNCATE64 = 193 + SYS_FTRUNCATE64 = 194 + SYS_STAT64 = 195 + SYS_LSTAT64 = 196 + SYS_FSTAT64 = 197 + SYS_LCHOWN32 = 198 + SYS_GETUID32 = 199 + SYS_GETGID32 = 200 + SYS_GETEUID32 = 201 + SYS_GETEGID32 = 202 + SYS_SETREUID32 = 203 + SYS_SETREGID32 = 204 + SYS_GETGROUPS32 = 205 + SYS_SETGROUPS32 = 206 + SYS_FCHOWN32 = 207 + SYS_SETRESUID32 = 208 + SYS_GETRESUID32 = 209 + SYS_SETRESGID32 = 210 + SYS_GETRESGID32 = 211 + SYS_CHOWN32 = 212 + SYS_SETUID32 = 213 + SYS_SETGID32 = 214 + SYS_SETFSUID32 = 215 + SYS_SETFSGID32 = 216 + SYS_PIVOT_ROOT = 217 + SYS_MINCORE = 218 + SYS_MADVISE = 219 + SYS_GETDENTS64 = 220 + SYS_FCNTL64 = 221 + SYS_GETTID = 224 + SYS_READAHEAD = 225 + SYS_SETXATTR = 226 + SYS_LSETXATTR = 227 + SYS_FSETXATTR = 228 + SYS_GETXATTR = 229 + SYS_LGETXATTR = 230 + SYS_FGETXATTR = 231 + SYS_LISTXATTR = 232 + SYS_LLISTXATTR = 233 + SYS_FLISTXATTR = 234 + SYS_REMOVEXATTR = 235 + SYS_LREMOVEXATTR = 236 + SYS_FREMOVEXATTR = 237 + SYS_TKILL = 238 + SYS_SENDFILE64 = 239 + SYS_FUTEX = 240 + SYS_SCHED_SETAFFINITY = 241 + SYS_SCHED_GETAFFINITY = 242 + SYS_SET_THREAD_AREA = 243 + SYS_GET_THREAD_AREA = 244 + SYS_IO_SETUP = 245 + SYS_IO_DESTROY = 246 + SYS_IO_GETEVENTS = 247 + SYS_IO_SUBMIT = 248 + SYS_IO_CANCEL = 249 + SYS_FADVISE64 = 250 + SYS_EXIT_GROUP = 252 + SYS_LOOKUP_DCOOKIE = 253 + SYS_EPOLL_CREATE = 254 + SYS_EPOLL_CTL = 255 + SYS_EPOLL_WAIT = 256 + SYS_REMAP_FILE_PAGES = 257 + SYS_SET_TID_ADDRESS = 258 + SYS_TIMER_CREATE = 259 + SYS_TIMER_SETTIME = 260 + SYS_TIMER_GETTIME = 261 + SYS_TIMER_GETOVERRUN = 262 + SYS_TIMER_DELETE = 263 + SYS_CLOCK_SETTIME = 264 + SYS_CLOCK_GETTIME = 265 + SYS_CLOCK_GETRES = 266 + SYS_CLOCK_NANOSLEEP = 267 + SYS_STATFS64 = 268 + SYS_FSTATFS64 = 269 + SYS_TGKILL = 270 + SYS_UTIMES = 271 + SYS_FADVISE64_64 = 272 + SYS_VSERVER = 273 + SYS_MBIND = 274 + SYS_GET_MEMPOLICY = 275 + SYS_SET_MEMPOLICY = 276 + SYS_MQ_OPEN = 277 + SYS_MQ_UNLINK = 278 + SYS_MQ_TIMEDSEND = 279 + SYS_MQ_TIMEDRECEIVE = 280 + SYS_MQ_NOTIFY = 281 + SYS_MQ_GETSETATTR = 282 + SYS_KEXEC_LOAD = 283 + SYS_WAITID = 284 + SYS_ADD_KEY = 286 + SYS_REQUEST_KEY = 287 + SYS_KEYCTL = 288 + SYS_IOPRIO_SET = 289 + SYS_IOPRIO_GET = 290 + SYS_INOTIFY_INIT = 291 + SYS_INOTIFY_ADD_WATCH = 292 + SYS_INOTIFY_RM_WATCH = 293 + SYS_MIGRATE_PAGES = 294 + SYS_OPENAT = 295 + SYS_MKDIRAT = 296 + SYS_MKNODAT = 297 + SYS_FCHOWNAT = 298 + SYS_FUTIMESAT = 299 + SYS_FSTATAT64 = 300 + SYS_UNLINKAT = 301 + SYS_RENAMEAT = 302 + SYS_LINKAT = 303 + SYS_SYMLINKAT = 304 + SYS_READLINKAT = 305 + SYS_FCHMODAT = 306 + SYS_FACCESSAT = 307 + SYS_PSELECT6 = 308 + SYS_PPOLL = 309 + SYS_UNSHARE = 310 + SYS_SET_ROBUST_LIST = 311 + SYS_GET_ROBUST_LIST = 312 + SYS_SPLICE = 313 + SYS_SYNC_FILE_RANGE = 314 + SYS_TEE = 315 + SYS_VMSPLICE = 316 + SYS_MOVE_PAGES = 317 + SYS_GETCPU = 318 + SYS_EPOLL_PWAIT = 319 + SYS_UTIMENSAT = 320 + SYS_SIGNALFD = 321 + SYS_TIMERFD_CREATE = 322 + SYS_EVENTFD = 323 + SYS_FALLOCATE = 324 + SYS_TIMERFD_SETTIME = 325 + SYS_TIMERFD_GETTIME = 326 + SYS_SIGNALFD4 = 327 + SYS_EVENTFD2 = 328 + SYS_EPOLL_CREATE1 = 329 + SYS_DUP3 = 330 + SYS_PIPE2 = 331 + SYS_INOTIFY_INIT1 = 332 + SYS_PREADV = 333 + SYS_PWRITEV = 334 + SYS_RT_TGSIGQUEUEINFO = 335 + SYS_PERF_EVENT_OPEN = 336 + SYS_RECVMMSG = 337 + SYS_FANOTIFY_INIT = 338 + SYS_FANOTIFY_MARK = 339 + SYS_PRLIMIT64 = 340 SYS_NAME_TO_HANDLE_AT = 341 SYS_OPEN_BY_HANDLE_AT = 342 SYS_CLOCK_ADJTIME = 343 @@ -576,6 +915,10 @@ const ( SYS_LSM_SET_SELF_ATTR = 460 SYS_LSM_LIST_MODULES = 461 SYS_MSEAL = 462 + SYS_SETXATTRAT = 463 + SYS_GETXATTRAT = 464 + SYS_LISTXATTRAT = 465 + SYS_REMOVEXATTRAT = 466 ) const ( @@ -1031,4 +1374,8 @@ const ( SNR_LSM_SET_SELF_ATTR SyscallNum = SYS_LSM_SET_SELF_ATTR SNR_LSM_LIST_MODULES SyscallNum = SYS_LSM_LIST_MODULES SNR_MSEAL SyscallNum = SYS_MSEAL + SNR_SETXATTRAT SyscallNum = SYS_SETXATTRAT + SNR_GETXATTRAT SyscallNum = SYS_GETXATTRAT + SNR_LISTXATTRAT SyscallNum = SYS_LISTXATTRAT + SNR_REMOVEXATTRAT SyscallNum = SYS_REMOVEXATTRAT ) diff --git a/ext/syscall_linux_amd64.go b/ext/syscall_linux_amd64.go index e4391217..38d99bda 100644 --- a/ext/syscall_linux_amd64.go +++ b/ext/syscall_linux_amd64.go @@ -3,8 +3,6 @@ package ext -import . "syscall" - var syscallNum = map[string]SyscallNum{ "read": SNR_READ, "write": SNR_WRITE, @@ -381,9 +379,316 @@ var syscallNum = map[string]SyscallNum{ "lsm_set_self_attr": SNR_LSM_SET_SELF_ATTR, "lsm_list_modules": SNR_LSM_LIST_MODULES, "mseal": SNR_MSEAL, + "setxattrat": SNR_SETXATTRAT, + "getxattrat": SNR_GETXATTRAT, + "listxattrat": SNR_LISTXATTRAT, + "removexattrat": SNR_REMOVEXATTRAT, } const ( + SYS_READ = 0 + SYS_WRITE = 1 + SYS_OPEN = 2 + SYS_CLOSE = 3 + SYS_STAT = 4 + SYS_FSTAT = 5 + SYS_LSTAT = 6 + SYS_POLL = 7 + SYS_LSEEK = 8 + SYS_MMAP = 9 + SYS_MPROTECT = 10 + SYS_MUNMAP = 11 + SYS_BRK = 12 + SYS_RT_SIGACTION = 13 + SYS_RT_SIGPROCMASK = 14 + SYS_RT_SIGRETURN = 15 + SYS_IOCTL = 16 + SYS_PREAD64 = 17 + SYS_PWRITE64 = 18 + SYS_READV = 19 + SYS_WRITEV = 20 + SYS_ACCESS = 21 + SYS_PIPE = 22 + SYS_SELECT = 23 + SYS_SCHED_YIELD = 24 + SYS_MREMAP = 25 + SYS_MSYNC = 26 + SYS_MINCORE = 27 + SYS_MADVISE = 28 + SYS_SHMGET = 29 + SYS_SHMAT = 30 + SYS_SHMCTL = 31 + SYS_DUP = 32 + SYS_DUP2 = 33 + SYS_PAUSE = 34 + SYS_NANOSLEEP = 35 + SYS_GETITIMER = 36 + SYS_ALARM = 37 + SYS_SETITIMER = 38 + SYS_GETPID = 39 + SYS_SENDFILE = 40 + SYS_SOCKET = 41 + SYS_CONNECT = 42 + SYS_ACCEPT = 43 + SYS_SENDTO = 44 + SYS_RECVFROM = 45 + SYS_SENDMSG = 46 + SYS_RECVMSG = 47 + SYS_SHUTDOWN = 48 + SYS_BIND = 49 + SYS_LISTEN = 50 + SYS_GETSOCKNAME = 51 + SYS_GETPEERNAME = 52 + SYS_SOCKETPAIR = 53 + SYS_SETSOCKOPT = 54 + SYS_GETSOCKOPT = 55 + SYS_CLONE = 56 + SYS_FORK = 57 + SYS_VFORK = 58 + SYS_EXECVE = 59 + SYS_EXIT = 60 + SYS_WAIT4 = 61 + SYS_KILL = 62 + SYS_UNAME = 63 + SYS_SEMGET = 64 + SYS_SEMOP = 65 + SYS_SEMCTL = 66 + SYS_SHMDT = 67 + SYS_MSGGET = 68 + SYS_MSGSND = 69 + SYS_MSGRCV = 70 + SYS_MSGCTL = 71 + SYS_FCNTL = 72 + SYS_FLOCK = 73 + SYS_FSYNC = 74 + SYS_FDATASYNC = 75 + SYS_TRUNCATE = 76 + SYS_FTRUNCATE = 77 + SYS_GETDENTS = 78 + SYS_GETCWD = 79 + SYS_CHDIR = 80 + SYS_FCHDIR = 81 + SYS_RENAME = 82 + SYS_MKDIR = 83 + SYS_RMDIR = 84 + SYS_CREAT = 85 + SYS_LINK = 86 + SYS_UNLINK = 87 + SYS_SYMLINK = 88 + SYS_READLINK = 89 + SYS_CHMOD = 90 + SYS_FCHMOD = 91 + SYS_CHOWN = 92 + SYS_FCHOWN = 93 + SYS_LCHOWN = 94 + SYS_UMASK = 95 + SYS_GETTIMEOFDAY = 96 + SYS_GETRLIMIT = 97 + SYS_GETRUSAGE = 98 + SYS_SYSINFO = 99 + SYS_TIMES = 100 + SYS_PTRACE = 101 + SYS_GETUID = 102 + SYS_SYSLOG = 103 + SYS_GETGID = 104 + SYS_SETUID = 105 + SYS_SETGID = 106 + SYS_GETEUID = 107 + SYS_GETEGID = 108 + SYS_SETPGID = 109 + SYS_GETPPID = 110 + SYS_GETPGRP = 111 + SYS_SETSID = 112 + SYS_SETREUID = 113 + SYS_SETREGID = 114 + SYS_GETGROUPS = 115 + SYS_SETGROUPS = 116 + SYS_SETRESUID = 117 + SYS_GETRESUID = 118 + SYS_SETRESGID = 119 + SYS_GETRESGID = 120 + SYS_GETPGID = 121 + SYS_SETFSUID = 122 + SYS_SETFSGID = 123 + SYS_GETSID = 124 + SYS_CAPGET = 125 + SYS_CAPSET = 126 + SYS_RT_SIGPENDING = 127 + SYS_RT_SIGTIMEDWAIT = 128 + SYS_RT_SIGQUEUEINFO = 129 + SYS_RT_SIGSUSPEND = 130 + SYS_SIGALTSTACK = 131 + SYS_UTIME = 132 + SYS_MKNOD = 133 + SYS_USELIB = 134 + SYS_PERSONALITY = 135 + SYS_USTAT = 136 + SYS_STATFS = 137 + SYS_FSTATFS = 138 + SYS_SYSFS = 139 + SYS_GETPRIORITY = 140 + SYS_SETPRIORITY = 141 + SYS_SCHED_SETPARAM = 142 + SYS_SCHED_GETPARAM = 143 + SYS_SCHED_SETSCHEDULER = 144 + SYS_SCHED_GETSCHEDULER = 145 + SYS_SCHED_GET_PRIORITY_MAX = 146 + SYS_SCHED_GET_PRIORITY_MIN = 147 + SYS_SCHED_RR_GET_INTERVAL = 148 + SYS_MLOCK = 149 + SYS_MUNLOCK = 150 + SYS_MLOCKALL = 151 + SYS_MUNLOCKALL = 152 + SYS_VHANGUP = 153 + SYS_MODIFY_LDT = 154 + SYS_PIVOT_ROOT = 155 + SYS__SYSCTL = 156 + SYS_PRCTL = 157 + SYS_ARCH_PRCTL = 158 + SYS_ADJTIMEX = 159 + SYS_SETRLIMIT = 160 + SYS_CHROOT = 161 + SYS_SYNC = 162 + SYS_ACCT = 163 + SYS_SETTIMEOFDAY = 164 + SYS_MOUNT = 165 + SYS_UMOUNT2 = 166 + SYS_SWAPON = 167 + SYS_SWAPOFF = 168 + SYS_REBOOT = 169 + SYS_SETHOSTNAME = 170 + SYS_SETDOMAINNAME = 171 + SYS_IOPL = 172 + SYS_IOPERM = 173 + SYS_CREATE_MODULE = 174 + SYS_INIT_MODULE = 175 + SYS_DELETE_MODULE = 176 + SYS_GET_KERNEL_SYMS = 177 + SYS_QUERY_MODULE = 178 + SYS_QUOTACTL = 179 + SYS_NFSSERVCTL = 180 + SYS_GETPMSG = 181 + SYS_PUTPMSG = 182 + SYS_AFS_SYSCALL = 183 + SYS_TUXCALL = 184 + SYS_SECURITY = 185 + SYS_GETTID = 186 + SYS_READAHEAD = 187 + SYS_SETXATTR = 188 + SYS_LSETXATTR = 189 + SYS_FSETXATTR = 190 + SYS_GETXATTR = 191 + SYS_LGETXATTR = 192 + SYS_FGETXATTR = 193 + SYS_LISTXATTR = 194 + SYS_LLISTXATTR = 195 + SYS_FLISTXATTR = 196 + SYS_REMOVEXATTR = 197 + SYS_LREMOVEXATTR = 198 + SYS_FREMOVEXATTR = 199 + SYS_TKILL = 200 + SYS_TIME = 201 + SYS_FUTEX = 202 + SYS_SCHED_SETAFFINITY = 203 + SYS_SCHED_GETAFFINITY = 204 + SYS_SET_THREAD_AREA = 205 + SYS_IO_SETUP = 206 + SYS_IO_DESTROY = 207 + SYS_IO_GETEVENTS = 208 + SYS_IO_SUBMIT = 209 + SYS_IO_CANCEL = 210 + SYS_GET_THREAD_AREA = 211 + SYS_LOOKUP_DCOOKIE = 212 + SYS_EPOLL_CREATE = 213 + SYS_EPOLL_CTL_OLD = 214 + SYS_EPOLL_WAIT_OLD = 215 + SYS_REMAP_FILE_PAGES = 216 + SYS_GETDENTS64 = 217 + SYS_SET_TID_ADDRESS = 218 + SYS_RESTART_SYSCALL = 219 + SYS_SEMTIMEDOP = 220 + SYS_FADVISE64 = 221 + SYS_TIMER_CREATE = 222 + SYS_TIMER_SETTIME = 223 + SYS_TIMER_GETTIME = 224 + SYS_TIMER_GETOVERRUN = 225 + SYS_TIMER_DELETE = 226 + SYS_CLOCK_SETTIME = 227 + SYS_CLOCK_GETTIME = 228 + SYS_CLOCK_GETRES = 229 + SYS_CLOCK_NANOSLEEP = 230 + SYS_EXIT_GROUP = 231 + SYS_EPOLL_WAIT = 232 + SYS_EPOLL_CTL = 233 + SYS_TGKILL = 234 + SYS_UTIMES = 235 + SYS_VSERVER = 236 + SYS_MBIND = 237 + SYS_SET_MEMPOLICY = 238 + SYS_GET_MEMPOLICY = 239 + SYS_MQ_OPEN = 240 + SYS_MQ_UNLINK = 241 + SYS_MQ_TIMEDSEND = 242 + SYS_MQ_TIMEDRECEIVE = 243 + SYS_MQ_NOTIFY = 244 + SYS_MQ_GETSETATTR = 245 + SYS_KEXEC_LOAD = 246 + SYS_WAITID = 247 + SYS_ADD_KEY = 248 + SYS_REQUEST_KEY = 249 + SYS_KEYCTL = 250 + SYS_IOPRIO_SET = 251 + SYS_IOPRIO_GET = 252 + SYS_INOTIFY_INIT = 253 + SYS_INOTIFY_ADD_WATCH = 254 + SYS_INOTIFY_RM_WATCH = 255 + SYS_MIGRATE_PAGES = 256 + SYS_OPENAT = 257 + SYS_MKDIRAT = 258 + SYS_MKNODAT = 259 + SYS_FCHOWNAT = 260 + SYS_FUTIMESAT = 261 + SYS_NEWFSTATAT = 262 + SYS_UNLINKAT = 263 + SYS_RENAMEAT = 264 + SYS_LINKAT = 265 + SYS_SYMLINKAT = 266 + SYS_READLINKAT = 267 + SYS_FCHMODAT = 268 + SYS_FACCESSAT = 269 + SYS_PSELECT6 = 270 + SYS_PPOLL = 271 + SYS_UNSHARE = 272 + SYS_SET_ROBUST_LIST = 273 + SYS_GET_ROBUST_LIST = 274 + SYS_SPLICE = 275 + SYS_TEE = 276 + SYS_SYNC_FILE_RANGE = 277 + SYS_VMSPLICE = 278 + SYS_MOVE_PAGES = 279 + SYS_UTIMENSAT = 280 + SYS_EPOLL_PWAIT = 281 + SYS_SIGNALFD = 282 + SYS_TIMERFD_CREATE = 283 + SYS_EVENTFD = 284 + SYS_FALLOCATE = 285 + SYS_TIMERFD_SETTIME = 286 + SYS_TIMERFD_GETTIME = 287 + SYS_ACCEPT4 = 288 + SYS_SIGNALFD4 = 289 + SYS_EVENTFD2 = 290 + SYS_EPOLL_CREATE1 = 291 + SYS_DUP3 = 292 + SYS_PIPE2 = 293 + SYS_INOTIFY_INIT1 = 294 + SYS_PREADV = 295 + SYS_PWRITEV = 296 + SYS_RT_TGSIGQUEUEINFO = 297 + SYS_PERF_EVENT_OPEN = 298 + SYS_RECVMMSG = 299 + SYS_FANOTIFY_INIT = 300 + SYS_FANOTIFY_MARK = 301 + SYS_PRLIMIT64 = 302 SYS_NAME_TO_HANDLE_AT = 303 SYS_OPEN_BY_HANDLE_AT = 304 SYS_CLOCK_ADJTIME = 305 @@ -456,6 +761,10 @@ const ( SYS_LSM_SET_SELF_ATTR = 460 SYS_LSM_LIST_MODULES = 461 SYS_MSEAL = 462 + SYS_SETXATTRAT = 463 + SYS_GETXATTRAT = 464 + SYS_LISTXATTRAT = 465 + SYS_REMOVEXATTRAT = 466 ) const ( @@ -834,4 +1143,8 @@ const ( SNR_LSM_SET_SELF_ATTR SyscallNum = SYS_LSM_SET_SELF_ATTR SNR_LSM_LIST_MODULES SyscallNum = SYS_LSM_LIST_MODULES SNR_MSEAL SyscallNum = SYS_MSEAL + SNR_SETXATTRAT SyscallNum = SYS_SETXATTRAT + SNR_GETXATTRAT SyscallNum = SYS_GETXATTRAT + SNR_LISTXATTRAT SyscallNum = SYS_LISTXATTRAT + SNR_REMOVEXATTRAT SyscallNum = SYS_REMOVEXATTRAT ) diff --git a/ext/syscall_linux_arm64.go b/ext/syscall_linux_arm64.go index 178c3182..f4a2b307 100644 --- a/ext/syscall_linux_arm64.go +++ b/ext/syscall_linux_arm64.go @@ -3,8 +3,6 @@ package ext -import . "syscall" - var syscallNum = map[string]SyscallNum{ "io_setup": SNR_IO_SETUP, "io_destroy": SNR_IO_DESTROY, @@ -324,9 +322,282 @@ var syscallNum = map[string]SyscallNum{ "lsm_set_self_attr": SNR_LSM_SET_SELF_ATTR, "lsm_list_modules": SNR_LSM_LIST_MODULES, "mseal": SNR_MSEAL, + "setxattrat": SNR_SETXATTRAT, + "getxattrat": SNR_GETXATTRAT, + "listxattrat": SNR_LISTXATTRAT, + "removexattrat": SNR_REMOVEXATTRAT, + "open_tree_attr": SNR_OPEN_TREE_ATTR, + "file_getattr": SNR_FILE_GETATTR, + "file_setattr": SNR_FILE_SETATTR, } const ( + SYS_IO_SETUP = 0 + SYS_IO_DESTROY = 1 + SYS_IO_SUBMIT = 2 + SYS_IO_CANCEL = 3 + SYS_IO_GETEVENTS = 4 + SYS_SETXATTR = 5 + SYS_LSETXATTR = 6 + SYS_FSETXATTR = 7 + SYS_GETXATTR = 8 + SYS_LGETXATTR = 9 + SYS_FGETXATTR = 10 + SYS_LISTXATTR = 11 + SYS_LLISTXATTR = 12 + SYS_FLISTXATTR = 13 + SYS_REMOVEXATTR = 14 + SYS_LREMOVEXATTR = 15 + SYS_FREMOVEXATTR = 16 + SYS_GETCWD = 17 + SYS_LOOKUP_DCOOKIE = 18 + SYS_EVENTFD2 = 19 + SYS_EPOLL_CREATE1 = 20 + SYS_EPOLL_CTL = 21 + SYS_EPOLL_PWAIT = 22 + SYS_DUP = 23 + SYS_DUP3 = 24 + SYS_FCNTL = 25 + SYS_INOTIFY_INIT1 = 26 + SYS_INOTIFY_ADD_WATCH = 27 + SYS_INOTIFY_RM_WATCH = 28 + SYS_IOCTL = 29 + SYS_IOPRIO_SET = 30 + SYS_IOPRIO_GET = 31 + SYS_FLOCK = 32 + SYS_MKNODAT = 33 + SYS_MKDIRAT = 34 + SYS_UNLINKAT = 35 + SYS_SYMLINKAT = 36 + SYS_LINKAT = 37 + SYS_RENAMEAT = 38 + SYS_UMOUNT2 = 39 + SYS_MOUNT = 40 + SYS_PIVOT_ROOT = 41 + SYS_NFSSERVCTL = 42 + SYS_STATFS = 43 + SYS_FSTATFS = 44 + SYS_TRUNCATE = 45 + SYS_FTRUNCATE = 46 + SYS_FALLOCATE = 47 + SYS_FACCESSAT = 48 + SYS_CHDIR = 49 + SYS_FCHDIR = 50 + SYS_CHROOT = 51 + SYS_FCHMOD = 52 + SYS_FCHMODAT = 53 + SYS_FCHOWNAT = 54 + SYS_FCHOWN = 55 + SYS_OPENAT = 56 + SYS_CLOSE = 57 + SYS_VHANGUP = 58 + SYS_PIPE2 = 59 + SYS_QUOTACTL = 60 + SYS_GETDENTS64 = 61 + SYS_LSEEK = 62 + SYS_READ = 63 + SYS_WRITE = 64 + SYS_READV = 65 + SYS_WRITEV = 66 + SYS_PREAD64 = 67 + SYS_PWRITE64 = 68 + SYS_PREADV = 69 + SYS_PWRITEV = 70 + SYS_SENDFILE = 71 + SYS_PSELECT6 = 72 + SYS_PPOLL = 73 + SYS_SIGNALFD4 = 74 + SYS_VMSPLICE = 75 + SYS_SPLICE = 76 + SYS_TEE = 77 + SYS_READLINKAT = 78 + SYS_NEWFSTATAT = 79 + SYS_FSTAT = 80 + SYS_SYNC = 81 + SYS_FSYNC = 82 + SYS_FDATASYNC = 83 + SYS_SYNC_FILE_RANGE = 84 + SYS_TIMERFD_CREATE = 85 + SYS_TIMERFD_SETTIME = 86 + SYS_TIMERFD_GETTIME = 87 + SYS_UTIMENSAT = 88 + SYS_ACCT = 89 + SYS_CAPGET = 90 + SYS_CAPSET = 91 + SYS_PERSONALITY = 92 + SYS_EXIT = 93 + SYS_EXIT_GROUP = 94 + SYS_WAITID = 95 + SYS_SET_TID_ADDRESS = 96 + SYS_UNSHARE = 97 + SYS_FUTEX = 98 + SYS_SET_ROBUST_LIST = 99 + SYS_GET_ROBUST_LIST = 100 + SYS_NANOSLEEP = 101 + SYS_GETITIMER = 102 + SYS_SETITIMER = 103 + SYS_KEXEC_LOAD = 104 + SYS_INIT_MODULE = 105 + SYS_DELETE_MODULE = 106 + SYS_TIMER_CREATE = 107 + SYS_TIMER_GETTIME = 108 + SYS_TIMER_GETOVERRUN = 109 + SYS_TIMER_SETTIME = 110 + SYS_TIMER_DELETE = 111 + SYS_CLOCK_SETTIME = 112 + SYS_CLOCK_GETTIME = 113 + SYS_CLOCK_GETRES = 114 + SYS_CLOCK_NANOSLEEP = 115 + SYS_SYSLOG = 116 + SYS_PTRACE = 117 + SYS_SCHED_SETPARAM = 118 + SYS_SCHED_SETSCHEDULER = 119 + SYS_SCHED_GETSCHEDULER = 120 + SYS_SCHED_GETPARAM = 121 + SYS_SCHED_SETAFFINITY = 122 + SYS_SCHED_GETAFFINITY = 123 + SYS_SCHED_YIELD = 124 + SYS_SCHED_GET_PRIORITY_MAX = 125 + SYS_SCHED_GET_PRIORITY_MIN = 126 + SYS_SCHED_RR_GET_INTERVAL = 127 + SYS_RESTART_SYSCALL = 128 + SYS_KILL = 129 + SYS_TKILL = 130 + SYS_TGKILL = 131 + SYS_SIGALTSTACK = 132 + SYS_RT_SIGSUSPEND = 133 + SYS_RT_SIGACTION = 134 + SYS_RT_SIGPROCMASK = 135 + SYS_RT_SIGPENDING = 136 + SYS_RT_SIGTIMEDWAIT = 137 + SYS_RT_SIGQUEUEINFO = 138 + SYS_RT_SIGRETURN = 139 + SYS_SETPRIORITY = 140 + SYS_GETPRIORITY = 141 + SYS_REBOOT = 142 + SYS_SETREGID = 143 + SYS_SETGID = 144 + SYS_SETREUID = 145 + SYS_SETUID = 146 + SYS_SETRESUID = 147 + SYS_GETRESUID = 148 + SYS_SETRESGID = 149 + SYS_GETRESGID = 150 + SYS_SETFSUID = 151 + SYS_SETFSGID = 152 + SYS_TIMES = 153 + SYS_SETPGID = 154 + SYS_GETPGID = 155 + SYS_GETSID = 156 + SYS_SETSID = 157 + SYS_GETGROUPS = 158 + SYS_SETGROUPS = 159 + SYS_UNAME = 160 + SYS_SETHOSTNAME = 161 + SYS_SETDOMAINNAME = 162 + SYS_GETRLIMIT = 163 + SYS_SETRLIMIT = 164 + SYS_GETRUSAGE = 165 + SYS_UMASK = 166 + SYS_PRCTL = 167 + SYS_GETCPU = 168 + SYS_GETTIMEOFDAY = 169 + SYS_SETTIMEOFDAY = 170 + SYS_ADJTIMEX = 171 + SYS_GETPID = 172 + SYS_GETPPID = 173 + SYS_GETUID = 174 + SYS_GETEUID = 175 + SYS_GETGID = 176 + SYS_GETEGID = 177 + SYS_GETTID = 178 + SYS_SYSINFO = 179 + SYS_MQ_OPEN = 180 + SYS_MQ_UNLINK = 181 + SYS_MQ_TIMEDSEND = 182 + SYS_MQ_TIMEDRECEIVE = 183 + SYS_MQ_NOTIFY = 184 + SYS_MQ_GETSETATTR = 185 + SYS_MSGGET = 186 + SYS_MSGCTL = 187 + SYS_MSGRCV = 188 + SYS_MSGSND = 189 + SYS_SEMGET = 190 + SYS_SEMCTL = 191 + SYS_SEMTIMEDOP = 192 + SYS_SEMOP = 193 + SYS_SHMGET = 194 + SYS_SHMCTL = 195 + SYS_SHMAT = 196 + SYS_SHMDT = 197 + SYS_SOCKET = 198 + SYS_SOCKETPAIR = 199 + SYS_BIND = 200 + SYS_LISTEN = 201 + SYS_ACCEPT = 202 + SYS_CONNECT = 203 + SYS_GETSOCKNAME = 204 + SYS_GETPEERNAME = 205 + SYS_SENDTO = 206 + SYS_RECVFROM = 207 + SYS_SETSOCKOPT = 208 + SYS_GETSOCKOPT = 209 + SYS_SHUTDOWN = 210 + SYS_SENDMSG = 211 + SYS_RECVMSG = 212 + SYS_READAHEAD = 213 + SYS_BRK = 214 + SYS_MUNMAP = 215 + SYS_MREMAP = 216 + SYS_ADD_KEY = 217 + SYS_REQUEST_KEY = 218 + SYS_KEYCTL = 219 + SYS_CLONE = 220 + SYS_EXECVE = 221 + SYS_MMAP = 222 + SYS_FADVISE64 = 223 + SYS_SWAPON = 224 + SYS_SWAPOFF = 225 + SYS_MPROTECT = 226 + SYS_MSYNC = 227 + SYS_MLOCK = 228 + SYS_MUNLOCK = 229 + SYS_MLOCKALL = 230 + SYS_MUNLOCKALL = 231 + SYS_MINCORE = 232 + SYS_MADVISE = 233 + SYS_REMAP_FILE_PAGES = 234 + SYS_MBIND = 235 + SYS_GET_MEMPOLICY = 236 + SYS_SET_MEMPOLICY = 237 + SYS_MIGRATE_PAGES = 238 + SYS_MOVE_PAGES = 239 + SYS_RT_TGSIGQUEUEINFO = 240 + SYS_PERF_EVENT_OPEN = 241 + SYS_ACCEPT4 = 242 + SYS_RECVMMSG = 243 + SYS_WAIT4 = 260 + SYS_PRLIMIT64 = 261 + SYS_FANOTIFY_INIT = 262 + SYS_FANOTIFY_MARK = 263 + SYS_NAME_TO_HANDLE_AT = 264 + SYS_OPEN_BY_HANDLE_AT = 265 + SYS_CLOCK_ADJTIME = 266 + SYS_SYNCFS = 267 + SYS_SETNS = 268 + SYS_SENDMMSG = 269 + SYS_PROCESS_VM_READV = 270 + SYS_PROCESS_VM_WRITEV = 271 + SYS_KCMP = 272 + SYS_FINIT_MODULE = 273 + SYS_SCHED_SETATTR = 274 + SYS_SCHED_GETATTR = 275 + SYS_RENAMEAT2 = 276 + SYS_SECCOMP = 277 + SYS_GETRANDOM = 278 + SYS_MEMFD_CREATE = 279 + SYS_BPF = 280 + SYS_EXECVEAT = 281 SYS_USERFAULTFD = 282 SYS_MEMBARRIER = 283 SYS_MLOCK2 = 284 @@ -379,6 +650,13 @@ const ( SYS_LSM_SET_SELF_ATTR = 460 SYS_LSM_LIST_MODULES = 461 SYS_MSEAL = 462 + SYS_SETXATTRAT = 463 + SYS_GETXATTRAT = 464 + SYS_LISTXATTRAT = 465 + SYS_REMOVEXATTRAT = 466 + SYS_OPEN_TREE_ATTR = 467 + SYS_FILE_GETATTR = 468 + SYS_FILE_SETATTR = 469 ) const ( @@ -700,4 +978,11 @@ const ( SNR_LSM_SET_SELF_ATTR SyscallNum = SYS_LSM_SET_SELF_ATTR SNR_LSM_LIST_MODULES SyscallNum = SYS_LSM_LIST_MODULES SNR_MSEAL SyscallNum = SYS_MSEAL + SNR_SETXATTRAT SyscallNum = SYS_SETXATTRAT + SNR_GETXATTRAT SyscallNum = SYS_GETXATTRAT + SNR_LISTXATTRAT SyscallNum = SYS_LISTXATTRAT + SNR_REMOVEXATTRAT SyscallNum = SYS_REMOVEXATTRAT + SNR_OPEN_TREE_ATTR SyscallNum = SYS_OPEN_TREE_ATTR + SNR_FILE_GETATTR SyscallNum = SYS_FILE_GETATTR + SNR_FILE_SETATTR SyscallNum = SYS_FILE_SETATTR ) diff --git a/ext/syscall_linux_riscv64.go b/ext/syscall_linux_riscv64.go index da3c3ae6..7ef22a16 100644 --- a/ext/syscall_linux_riscv64.go +++ b/ext/syscall_linux_riscv64.go @@ -1,10 +1,8 @@ -// mksysnum_linux.pl /usr/include/riscv64-linux-gnu/asm/unistd.h +// mksysnum_linux.pl /usr/include/asm/unistd.h // Code generated by the command above; DO NOT EDIT. package ext -import . "syscall" - var syscallNum = map[string]SyscallNum{ "io_setup": SNR_IO_SETUP, "io_destroy": SNR_IO_DESTROY, @@ -249,6 +247,8 @@ var syscallNum = map[string]SyscallNum{ "perf_event_open": SNR_PERF_EVENT_OPEN, "accept4": SNR_ACCEPT4, "recvmmsg": SNR_RECVMMSG, + "riscv_hwprobe": SNR_RISCV_HWPROBE, + "riscv_flush_icache": SNR_RISCV_FLUSH_ICACHE, "wait4": SNR_WAIT4, "prlimit64": SNR_PRLIMIT64, "fanotify_init": SNR_FANOTIFY_INIT, @@ -327,9 +327,277 @@ var syscallNum = map[string]SyscallNum{ "getxattrat": SNR_GETXATTRAT, "listxattrat": SNR_LISTXATTRAT, "removexattrat": SNR_REMOVEXATTRAT, + "open_tree_attr": SNR_OPEN_TREE_ATTR, } const ( + SYS_IO_SETUP = 0 + SYS_IO_DESTROY = 1 + SYS_IO_SUBMIT = 2 + SYS_IO_CANCEL = 3 + SYS_IO_GETEVENTS = 4 + SYS_SETXATTR = 5 + SYS_LSETXATTR = 6 + SYS_FSETXATTR = 7 + SYS_GETXATTR = 8 + SYS_LGETXATTR = 9 + SYS_FGETXATTR = 10 + SYS_LISTXATTR = 11 + SYS_LLISTXATTR = 12 + SYS_FLISTXATTR = 13 + SYS_REMOVEXATTR = 14 + SYS_LREMOVEXATTR = 15 + SYS_FREMOVEXATTR = 16 + SYS_GETCWD = 17 + SYS_LOOKUP_DCOOKIE = 18 + SYS_EVENTFD2 = 19 + SYS_EPOLL_CREATE1 = 20 + SYS_EPOLL_CTL = 21 + SYS_EPOLL_PWAIT = 22 + SYS_DUP = 23 + SYS_DUP3 = 24 + SYS_FCNTL = 25 + SYS_INOTIFY_INIT1 = 26 + SYS_INOTIFY_ADD_WATCH = 27 + SYS_INOTIFY_RM_WATCH = 28 + SYS_IOCTL = 29 + SYS_IOPRIO_SET = 30 + SYS_IOPRIO_GET = 31 + SYS_FLOCK = 32 + SYS_MKNODAT = 33 + SYS_MKDIRAT = 34 + SYS_UNLINKAT = 35 + SYS_SYMLINKAT = 36 + SYS_LINKAT = 37 + SYS_UMOUNT2 = 39 + SYS_MOUNT = 40 + SYS_PIVOT_ROOT = 41 + SYS_NFSSERVCTL = 42 + SYS_STATFS = 43 + SYS_FSTATFS = 44 + SYS_TRUNCATE = 45 + SYS_FTRUNCATE = 46 + SYS_FALLOCATE = 47 + SYS_FACCESSAT = 48 + SYS_CHDIR = 49 + SYS_FCHDIR = 50 + SYS_CHROOT = 51 + SYS_FCHMOD = 52 + SYS_FCHMODAT = 53 + SYS_FCHOWNAT = 54 + SYS_FCHOWN = 55 + SYS_OPENAT = 56 + SYS_CLOSE = 57 + SYS_VHANGUP = 58 + SYS_PIPE2 = 59 + SYS_QUOTACTL = 60 + SYS_GETDENTS64 = 61 + SYS_LSEEK = 62 + SYS_READ = 63 + SYS_WRITE = 64 + SYS_READV = 65 + SYS_WRITEV = 66 + SYS_PREAD64 = 67 + SYS_PWRITE64 = 68 + SYS_PREADV = 69 + SYS_PWRITEV = 70 + SYS_SENDFILE = 71 + SYS_PSELECT6 = 72 + SYS_PPOLL = 73 + SYS_SIGNALFD4 = 74 + SYS_VMSPLICE = 75 + SYS_SPLICE = 76 + SYS_TEE = 77 + SYS_READLINKAT = 78 + SYS_NEWFSTATAT = 79 + SYS_FSTAT = 80 + SYS_SYNC = 81 + SYS_FSYNC = 82 + SYS_FDATASYNC = 83 + SYS_SYNC_FILE_RANGE = 84 + SYS_TIMERFD_CREATE = 85 + SYS_TIMERFD_SETTIME = 86 + SYS_TIMERFD_GETTIME = 87 + SYS_UTIMENSAT = 88 + SYS_ACCT = 89 + SYS_CAPGET = 90 + SYS_CAPSET = 91 + SYS_PERSONALITY = 92 + SYS_EXIT = 93 + SYS_EXIT_GROUP = 94 + SYS_WAITID = 95 + SYS_SET_TID_ADDRESS = 96 + SYS_UNSHARE = 97 + SYS_FUTEX = 98 + SYS_SET_ROBUST_LIST = 99 + SYS_GET_ROBUST_LIST = 100 + SYS_NANOSLEEP = 101 + SYS_GETITIMER = 102 + SYS_SETITIMER = 103 + SYS_KEXEC_LOAD = 104 + SYS_INIT_MODULE = 105 + SYS_DELETE_MODULE = 106 + SYS_TIMER_CREATE = 107 + SYS_TIMER_GETTIME = 108 + SYS_TIMER_GETOVERRUN = 109 + SYS_TIMER_SETTIME = 110 + SYS_TIMER_DELETE = 111 + SYS_CLOCK_SETTIME = 112 + SYS_CLOCK_GETTIME = 113 + SYS_CLOCK_GETRES = 114 + SYS_CLOCK_NANOSLEEP = 115 + SYS_SYSLOG = 116 + SYS_PTRACE = 117 + SYS_SCHED_SETPARAM = 118 + SYS_SCHED_SETSCHEDULER = 119 + SYS_SCHED_GETSCHEDULER = 120 + SYS_SCHED_GETPARAM = 121 + SYS_SCHED_SETAFFINITY = 122 + SYS_SCHED_GETAFFINITY = 123 + SYS_SCHED_YIELD = 124 + SYS_SCHED_GET_PRIORITY_MAX = 125 + SYS_SCHED_GET_PRIORITY_MIN = 126 + SYS_SCHED_RR_GET_INTERVAL = 127 + SYS_RESTART_SYSCALL = 128 + SYS_KILL = 129 + SYS_TKILL = 130 + SYS_TGKILL = 131 + SYS_SIGALTSTACK = 132 + SYS_RT_SIGSUSPEND = 133 + SYS_RT_SIGACTION = 134 + SYS_RT_SIGPROCMASK = 135 + SYS_RT_SIGPENDING = 136 + SYS_RT_SIGTIMEDWAIT = 137 + SYS_RT_SIGQUEUEINFO = 138 + SYS_RT_SIGRETURN = 139 + SYS_SETPRIORITY = 140 + SYS_GETPRIORITY = 141 + SYS_REBOOT = 142 + SYS_SETREGID = 143 + SYS_SETGID = 144 + SYS_SETREUID = 145 + SYS_SETUID = 146 + SYS_SETRESUID = 147 + SYS_GETRESUID = 148 + SYS_SETRESGID = 149 + SYS_GETRESGID = 150 + SYS_SETFSUID = 151 + SYS_SETFSGID = 152 + SYS_TIMES = 153 + SYS_SETPGID = 154 + SYS_GETPGID = 155 + SYS_GETSID = 156 + SYS_SETSID = 157 + SYS_GETGROUPS = 158 + SYS_SETGROUPS = 159 + SYS_UNAME = 160 + SYS_SETHOSTNAME = 161 + SYS_SETDOMAINNAME = 162 + SYS_GETRLIMIT = 163 + SYS_SETRLIMIT = 164 + SYS_GETRUSAGE = 165 + SYS_UMASK = 166 + SYS_PRCTL = 167 + SYS_GETCPU = 168 + SYS_GETTIMEOFDAY = 169 + SYS_SETTIMEOFDAY = 170 + SYS_ADJTIMEX = 171 + SYS_GETPID = 172 + SYS_GETPPID = 173 + SYS_GETUID = 174 + SYS_GETEUID = 175 + SYS_GETGID = 176 + SYS_GETEGID = 177 + SYS_GETTID = 178 + SYS_SYSINFO = 179 + SYS_MQ_OPEN = 180 + SYS_MQ_UNLINK = 181 + SYS_MQ_TIMEDSEND = 182 + SYS_MQ_TIMEDRECEIVE = 183 + SYS_MQ_NOTIFY = 184 + SYS_MQ_GETSETATTR = 185 + SYS_MSGGET = 186 + SYS_MSGCTL = 187 + SYS_MSGRCV = 188 + SYS_MSGSND = 189 + SYS_SEMGET = 190 + SYS_SEMCTL = 191 + SYS_SEMTIMEDOP = 192 + SYS_SEMOP = 193 + SYS_SHMGET = 194 + SYS_SHMCTL = 195 + SYS_SHMAT = 196 + SYS_SHMDT = 197 + SYS_SOCKET = 198 + SYS_SOCKETPAIR = 199 + SYS_BIND = 200 + SYS_LISTEN = 201 + SYS_ACCEPT = 202 + SYS_CONNECT = 203 + SYS_GETSOCKNAME = 204 + SYS_GETPEERNAME = 205 + SYS_SENDTO = 206 + SYS_RECVFROM = 207 + SYS_SETSOCKOPT = 208 + SYS_GETSOCKOPT = 209 + SYS_SHUTDOWN = 210 + SYS_SENDMSG = 211 + SYS_RECVMSG = 212 + SYS_READAHEAD = 213 + SYS_BRK = 214 + SYS_MUNMAP = 215 + SYS_MREMAP = 216 + SYS_ADD_KEY = 217 + SYS_REQUEST_KEY = 218 + SYS_KEYCTL = 219 + SYS_CLONE = 220 + SYS_EXECVE = 221 + SYS_MMAP = 222 + SYS_FADVISE64 = 223 + SYS_SWAPON = 224 + SYS_SWAPOFF = 225 + SYS_MPROTECT = 226 + SYS_MSYNC = 227 + SYS_MLOCK = 228 + SYS_MUNLOCK = 229 + SYS_MLOCKALL = 230 + SYS_MUNLOCKALL = 231 + SYS_MINCORE = 232 + SYS_MADVISE = 233 + SYS_REMAP_FILE_PAGES = 234 + SYS_MBIND = 235 + SYS_GET_MEMPOLICY = 236 + SYS_SET_MEMPOLICY = 237 + SYS_MIGRATE_PAGES = 238 + SYS_MOVE_PAGES = 239 + SYS_RT_TGSIGQUEUEINFO = 240 + SYS_PERF_EVENT_OPEN = 241 + SYS_ACCEPT4 = 242 + SYS_RECVMMSG = 243 + SYS_RISCV_HWPROBE = 258 + SYS_RISCV_FLUSH_ICACHE = 259 + SYS_WAIT4 = 260 + SYS_PRLIMIT64 = 261 + SYS_FANOTIFY_INIT = 262 + SYS_FANOTIFY_MARK = 263 + SYS_NAME_TO_HANDLE_AT = 264 + SYS_OPEN_BY_HANDLE_AT = 265 + SYS_CLOCK_ADJTIME = 266 + SYS_SYNCFS = 267 + SYS_SETNS = 268 + SYS_SENDMMSG = 269 + SYS_PROCESS_VM_READV = 270 + SYS_PROCESS_VM_WRITEV = 271 + SYS_KCMP = 272 + SYS_FINIT_MODULE = 273 + SYS_SCHED_SETATTR = 274 + SYS_SCHED_GETATTR = 275 + SYS_RENAMEAT2 = 276 + SYS_SECCOMP = 277 + SYS_GETRANDOM = 278 + SYS_MEMFD_CREATE = 279 + SYS_BPF = 280 + SYS_EXECVEAT = 281 SYS_USERFAULTFD = 282 SYS_MEMBARRIER = 283 SYS_MLOCK2 = 284 @@ -635,6 +903,8 @@ const ( SNR_PERF_EVENT_OPEN SyscallNum = SYS_PERF_EVENT_OPEN SNR_ACCEPT4 SyscallNum = SYS_ACCEPT4 SNR_RECVMMSG SyscallNum = SYS_RECVMMSG + SNR_RISCV_HWPROBE SyscallNum = SYS_RISCV_HWPROBE + SNR_RISCV_FLUSH_ICACHE SyscallNum = SYS_RISCV_FLUSH_ICACHE SNR_WAIT4 SyscallNum = SYS_WAIT4 SNR_PRLIMIT64 SyscallNum = SYS_PRLIMIT64 SNR_FANOTIFY_INIT SyscallNum = SYS_FANOTIFY_INIT diff --git a/ext/syscall_other.go b/ext/syscall_other.go new file mode 100644 index 00000000..b2518d64 --- /dev/null +++ b/ext/syscall_other.go @@ -0,0 +1,11 @@ +//go:build arm64 || riscv64 || mips || mipsle || mips64 || mips64le || ppc64 || ppc64le || s390x || loong64 + +package ext + +import "syscall" + +const ( + O_PATH = syscall.O_PATH + + PR_SET_NO_NEW_PRIVS = syscall.PR_SET_NO_NEW_PRIVS +) @@ -40,6 +40,8 @@ var ( AbsDev = unsafeAbs(Dev) // AbsDevShm is [DevShm] as [check.Absolute]. AbsDevShm = unsafeAbs(DevShm) + // AbsDevKvm is [DevKvm] as [check.Absolute]. + AbsDevKvm = unsafeAbs(DevKvm) // AbsProc is [Proc] as [check.Absolute]. AbsProc = unsafeAbs(Proc) // AbsProcSys is [ProcSys] as [check.Absolute]. @@ -35,6 +35,8 @@ const ( // DevShm is the place for POSIX shared memory segments, as created via // shm_open(3). DevShm = "/dev/shm/" + // DevKvm is the KVM system device. + DevKvm = "/dev/kvm" // Proc points to a virtual kernel file system exposing the process list and // other functionality. Proc = "/proc/" diff --git a/hst/container.go b/hst/container.go index 824b0982..c4cd2712 100644 --- a/hst/container.go +++ b/hst/container.go @@ -141,6 +141,13 @@ type ContainerConfig struct { // [WaitDelayDefault]. WaitDelay time.Duration `json:"wait_delay,omitempty"` + // Duration in nanoseconds to wait for lingering processes after the initial + // process terminates. + // + // Defaults to five seconds if zero or greater than five seconds. Values + // lesser than zero is equivalent to zero. + AdoptWaitDelay time.Duration `json:"adopt_wait_delay,omitempty"` + // Initial process environment variables. Env map[string]string `json:"env"` diff --git a/internal/acl/acl.go b/internal/acl/acl.go index 366ad533..d5b53095 100644 --- a/internal/acl/acl.go +++ b/internal/acl/acl.go @@ -8,26 +8,103 @@ package acl */ import "C" +import ( + "os" + + "hakurei.app/ext" +) + +// Perm represents acl_perm_t. type Perm C.acl_perm_t const ( - Read Perm = C.ACL_READ - Write Perm = C.ACL_WRITE + // Read represents ACL_READ. + Read Perm = C.ACL_READ + // Write represents ACL_WRITE. + Write Perm = C.ACL_WRITE + // Execute represents ACL_EXECUTE. Execute Perm = C.ACL_EXECUTE ) +// Status is the outcome of an ACL update operation. +type Status = C.enum_hakurei_acl_status + +// An Error describes an error encountered during [Update]. +type Error struct { + // Pathname of the file to update ACL for. + Path string + // Function that returned the error. + Op Status + // Underlying errno. + Err error +} + +// Unwrap returns an [os.PathError] describing e. +func (e Error) Unwrap() error { + v := os.PathError{Path: e.Path, Err: e.Err} + switch e.Op { + case C.HAKUREI_ACL_OK: + return os.ErrInvalid + + case C.HAKUREI_ACL_GET_FILE: + v.Op = "acl_get_file" + case C.HAKUREI_ACL_GET_TAG_TYPE: + v.Op = "acl_get_tag_type" + case C.HAKUREI_ACL_GET_QUALIFIER: + v.Op = "acl_get_qualifier" + case C.HAKUREI_ACL_DELETE_ENTRY: + v.Op = "acl_delete_entry" + case C.HAKUREI_ACL_CREATE_ENTRY: + v.Op = "acl_create_entry" + case C.HAKUREI_ACL_GET_PERMSET: + v.Op = "acl_get_permset" + case C.HAKUREI_ACL_ADD_PERM: + v.Op = "acl_add_perm" + case C.HAKUREI_ACL_SET_TAG_TYPE: + v.Op = "acl_set_tag_type" + case C.HAKUREI_ACL_SET_QUALIFIER: + v.Op = "acl_set_qualifier" + case C.HAKUREI_ACL_CALC_MASK: + v.Op = "acl_calc_mask" + case C.HAKUREI_ACL_VALID: + v.Op = "acl_valid" + case C.HAKUREI_ACL_SET_FILE: + v.Op = "acl_set_file" + case C.HAKUREI_ACL_GET_PERM: + v.Op = "acl_get_perm" + case C.HAKUREI_ACL_DUP: + v.Op = "acl_dup" + + default: // unreachable + v.Op = "setfacl" + } + return &v +} + +// Error returns the message of the resulting [os.PathError]. +func (e Error) Error() string { + return e.Unwrap().Error() +} + // Update replaces ACL_USER entry with qualifier uid. -func Update(name string, uid int, perms ...Perm) error { - var p *Perm - if len(perms) > 0 { - p = &perms[0] +func Update(name string, uid int, perms Perm) error { + f, _err := os.OpenFile(name, ext.O_PATH, 0) + if _err != nil { + return _err } - r, err := C.hakurei_acl_update_file_by_uid( - C.CString(name), + r, err := C.hakurei_update_acl( + C.int(f.Fd()), C.uid_t(uid), - (*C.acl_perm_t)(p), - C.size_t(len(perms)), + C.acl_perm_t(perms), ) - return newAclPathError(name, int(r), err) + + if _err = f.Close(); _err != nil { + return _err + } + + if r == C.HAKUREI_ACL_OK { + return nil + } + return &Error{name, r, err} } diff --git a/internal/acl/acl_test.go b/internal/acl/acl_test.go index 60c4b702..14f0ca18 100644 --- a/internal/acl/acl_test.go +++ b/internal/acl/acl_test.go @@ -18,93 +18,131 @@ import ( "hakurei.app/internal/info" ) -const testFileName = "acl.test" - var ( uid = os.Geteuid() - cred = int32(os.Geteuid()) + cred = int32(uid) ) +// testFile returns the pathname of a newly created test file. +func testFile(t *testing.T) string { + name := filepath.Join(t.TempDir(), "f") + f, err := os.Create(name) + if err != nil { + t.Fatal(err) + } else if err = f.Close(); err != nil { + t.Fatal(err) + } + return name +} + func TestUpdate(t *testing.T) { if info.CanDegrade { name := filepath.Join(t.TempDir(), "check-degrade") if err := os.WriteFile(name, nil, 0); err != nil { t.Fatal(err) } - if err := acl.Update(name, os.Geteuid()); errors.Is(err, syscall.ENOTSUP) { + if err := acl.Update(name, uid, 0); errors.Is(err, syscall.ENOTSUP) { t.Skip(err) } } - testFilePath := filepath.Join(t.TempDir(), testFileName) + t.Parallel() - if f, err := os.Create(testFilePath); err != nil { - t.Fatalf("Create: error = %v", err) - } else { - if err = f.Close(); err != nil { - t.Fatalf("Close: error = %v", err) - } + pathname := testFile(t) + cur := getfacl(t, pathname) + if len(cur) != 3 { + t.Fatalf("unexpected test file acl length %d", len(cur)) } - defer func() { - if err := os.Remove(testFilePath); err != nil { - t.Fatalf("Remove: error = %v", err) - } - }() - cur := getfacl(t, testFilePath) + if err := acl.Update(pathname, uid, 0); err != nil { + t.Fatalf("(clear) Update: error = %v", err) + } + if cur = getfacl(t, pathname); len(cur) != 4 { + t.Fatalf("(clear) Update: %v", cur) + } - t.Run("default entry count", func(t *testing.T) { - if len(cur) != 3 { - t.Fatalf("unexpected test file acl length %d", len(cur)) - } - }) + if err := acl.Update(pathname, uid, 0); err != nil { + t.Fatalf("(clear*) Update: error = %v", err) + } + if val := getfacl(t, pathname); !reflect.DeepEqual(val, cur) { + t.Fatalf("(clear*) Update: %v, want %v", val, cur) + } - t.Run("default clear mask", func(t *testing.T) { - if err := acl.Update(testFilePath, uid); err != nil { - t.Fatalf("Update: error = %v", err) - } - if cur = getfacl(t, testFilePath); len(cur) != 4 { - t.Fatalf("Update: %v", cur) - } - }) + testUpdate(t, "r--", "", cur, acl.Read, true) + testUpdate(t, "-w-", "", cur, acl.Write, true) + testUpdate(t, "--x", "", cur, acl.Execute, true) - t.Run("default clear consistency", func(t *testing.T) { - if err := acl.Update(testFilePath, uid); err != nil { - t.Fatalf("Update: error = %v", err) - } - if val := getfacl(t, testFilePath); !reflect.DeepEqual(val, cur) { - t.Fatalf("Update: %v, want %v", val, cur) - } - }) + testUpdate( + t, "-wx", "", cur, + acl.Write|acl.Execute, + true, + ) + testUpdate( + t, "r-x", "", cur, + acl.Read|acl.Execute, + true, + ) + testUpdate( + t, "rw-", "", cur, + acl.Read|acl.Write, + true, + ) + testUpdate( + t, "rwx", "", cur, + acl.Read|acl.Write|acl.Execute, + true, + ) - testUpdate(t, testFilePath, "r--", cur, fAclPermRead, acl.Read) - testUpdate(t, testFilePath, "-w-", cur, fAclPermWrite, acl.Write) - testUpdate(t, testFilePath, "--x", cur, fAclPermExecute, acl.Execute) - testUpdate(t, testFilePath, "-wx", cur, fAclPermWrite|fAclPermExecute, acl.Write, acl.Execute) - testUpdate(t, testFilePath, "r-x", cur, fAclPermRead|fAclPermExecute, acl.Read, acl.Execute) - testUpdate(t, testFilePath, "rw-", cur, fAclPermRead|fAclPermWrite, acl.Read, acl.Write) - testUpdate(t, testFilePath, "rwx", cur, fAclPermRead|fAclPermWrite|fAclPermExecute, acl.Read, acl.Write, acl.Execute) + for i := range 16 { + testUpdate( + t, "r-x", " "+strconv.Itoa(i), cur, + acl.Read|acl.Execute, + false, + ) + } + + testUpdate( + t, "rwx", " cleanup", cur, + acl.Read|acl.Write|acl.Execute, + true, + ) } -func testUpdate(t *testing.T, testFilePath, name string, cur []*getFAclResp, val fAclPerm, perms ...acl.Perm) { - t.Run(name, func(t *testing.T) { - t.Cleanup(func() { - if err := acl.Update(testFilePath, uid); err != nil { - t.Fatalf("Update: error = %v", err) - } - if v := getfacl(t, testFilePath); !reflect.DeepEqual(v, cur) { - t.Fatalf("Update: %v, want %v", v, cur) - } - }) +func testUpdate( + t *testing.T, + name, suffix string, + cur []*getFACLResp, + perms acl.Perm, + cleanup bool, +) { + t.Run(name+suffix, func(t *testing.T) { + t.Parallel() - if err := acl.Update(testFilePath, uid, perms...); err != nil { + pathname := testFile(t) + if cleanup { + t.Cleanup(func() { + if err := acl.Update(pathname, uid, 0); err != nil { + t.Fatalf("Update: error = %v", err) + } + if v := getfacl(t, pathname); !reflect.DeepEqual(v, cur) { + t.Fatalf("Update: %v, want %v", v, cur) + } + }) + } + + if err := acl.Update(pathname, uid, perms); err != nil { t.Fatalf("Update: error = %v", err) } - r := respByCred(getfacl(t, testFilePath), fAclTypeUser, cred) + r := respByCred(getfacl(t, pathname), fAclTypeUser, cred) if r == nil { t.Fatalf("Update did not add an ACL entry") } - if !r.equals(fAclTypeUser, cred, val) { + + if !r.equals(&getFACLResp{ + typ: fAclTypeUser, + cred: cred, + val: perms, + }) { t.Fatalf("Update(%s) = %s", name, r) } }) @@ -113,31 +151,24 @@ func testUpdate(t *testing.T, testFilePath, name string, cur []*getFAclResp, val type ( getFAclInvocation struct { cmd *exec.Cmd - val []*getFAclResp + val []*getFACLResp pe []error } - getFAclResp struct { + getFACLResp struct { typ fAclType cred int32 - val fAclPerm + val acl.Perm raw []byte } - fAclPerm uintptr fAclType uint8 ) const fAclBufSize = 16 const ( - fAclPermRead fAclPerm = 1 << iota - fAclPermWrite - fAclPermExecute -) - -const ( fAclTypeUser fAclType = iota fAclTypeGroup fAclTypeMask @@ -149,7 +180,13 @@ func (c *getFAclInvocation) run(name string) error { panic("attempted to run twice") } - c.cmd = exec.Command("getfacl", "--omit-header", "--absolute-names", "--numeric", name) + c.cmd = exec.Command( + "getfacl", + "--omit-header", + "--absolute-names", + "--numeric", + name, + ) c.cmd.Stderr = os.Stderr scanErr := make(chan error, 1) @@ -166,18 +203,17 @@ func (c *getFAclInvocation) run(name string) error { return errors.Join(<-scanErr, c.cmd.Wait()) } -func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) { - c.val = make([]*getFAclResp, 0, 4+fAclBufSize) +func (c *getFAclInvocation) parse(r io.Reader, scanErr chan error) { + c.val = make([]*getFACLResp, 0, 4+fAclBufSize) - s := bufio.NewScanner(pipe) + s := bufio.NewScanner(r) for s.Scan() { fields := bytes.SplitN(s.Bytes(), []byte{':'}, 3) if len(fields) != 3 { continue } - resp := getFAclResp{} - + var resp getFACLResp switch string(fields[0]) { case "user": resp.typ = fAclTypeUser @@ -213,7 +249,7 @@ func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) { } else { switch fields[2][0] { case 'r': - resp.val |= fAclPermRead + resp.val |= acl.Read case '-': default: c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][0])) @@ -221,7 +257,7 @@ func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) { } switch fields[2][1] { case 'w': - resp.val |= fAclPermWrite + resp.val |= acl.Write case '-': default: c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][1])) @@ -229,7 +265,7 @@ func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) { } switch fields[2][2] { case 'x': - resp.val |= fAclPermExecute + resp.val |= acl.Execute case '-': default: c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][2])) @@ -244,7 +280,7 @@ func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) { scanErr <- s.Err() } -func (r *getFAclResp) String() string { +func (r *getFACLResp) String() string { if r.raw != nil && len(r.raw) > 0 { return string(r.raw) } @@ -252,11 +288,11 @@ func (r *getFAclResp) String() string { return "(user-initialised resp value)" } -func (r *getFAclResp) equals(typ fAclType, cred int32, val fAclPerm) bool { - return r.typ == typ && r.cred == cred && r.val == val +func (r *getFACLResp) equals(v *getFACLResp) bool { + return r.typ == v.typ && r.cred == v.cred && r.val == v.val } -func getfacl(t *testing.T, name string) []*getFAclResp { +func getfacl(t *testing.T, name string) []*getFACLResp { c := new(getFAclInvocation) if err := c.run(name); err != nil { t.Fatalf("getfacl: error = %v", err) @@ -267,7 +303,7 @@ func getfacl(t *testing.T, name string) []*getFAclResp { return c.val } -func respByCred(v []*getFAclResp, typ fAclType, cred int32) *getFAclResp { +func respByCred(v []*getFACLResp, typ fAclType, cred int32) *getFACLResp { j := -1 for i, r := range v { if r.typ == typ && r.cred == cred { diff --git a/internal/acl/libacl-helper.c b/internal/acl/libacl-helper.c index 259dfb2b..c56e6436 100644 --- a/internal/acl/libacl-helper.c +++ b/internal/acl/libacl-helper.c @@ -1,90 +1,133 @@ +#define _GNU_SOURCE /* Definition of AT_* constants */ + #include "libacl-helper.h" #include <acl/libacl.h> #include <stdbool.h> #include <stdlib.h> +#include <fcntl.h> #include <sys/acl.h> -int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid, - acl_perm_t *perms, size_t plen) { - int ret; - bool v; - int i; - acl_t acl; - acl_entry_t entry; - acl_tag_t tag_type; - void *qualifier_p; - acl_permset_t permset; +enum hakurei_acl_status hakurei_update_acl(int fd, uid_t uid, acl_perm_t perms) +{ + enum hakurei_acl_status ret; + + bool v; + int i; + acl_t acl, acl_d = NULL; + acl_entry_t entry; + acl_tag_t tag_type; + void *qualifier_p; + acl_permset_t permset; + acl_perm_t has_perms; + + ret = HAKUREI_ACL_GET_FILE; + acl = acl_get_file_at(fd, "", AT_EMPTY_PATH, ACL_TYPE_ACCESS); + if (acl == NULL) + goto out; + + /* prune entries by uid */ + for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1; + i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) { + ret = HAKUREI_ACL_GET_TAG_TYPE; + if (acl_get_tag_type(entry, &tag_type) != 0) + goto out; + if (tag_type != ACL_USER) + continue; + + ret = HAKUREI_ACL_GET_QUALIFIER; + qualifier_p = acl_get_qualifier(entry); + if (qualifier_p == NULL) + goto out; + v = *(uid_t *)qualifier_p == uid; + acl_free(qualifier_p); + + if (!v) + continue; + + ret = HAKUREI_ACL_GET_PERMSET; + if (acl_get_permset(entry, &permset) != 0) + goto out; + + if (perms == 0) + goto delete; - ret = -1; /* acl_get_file */ - acl = acl_get_file(path_p, ACL_TYPE_ACCESS); - if (acl == NULL) - goto out; + /* this is quite ugly, but the standard makes it impossible to + * directly obtain these bits */ + has_perms = 0; +#define COPY_PERM_BIT(perm) \ + do { \ + ret = HAKUREI_ACL_GET_PERM; \ + i = acl_get_perm(permset, perm); \ + if (i < 0) \ + goto out; \ + if (i > 0) \ + has_perms |= perm; \ + } while (0) + COPY_PERM_BIT(ACL_READ); + COPY_PERM_BIT(ACL_WRITE); + COPY_PERM_BIT(ACL_EXECUTE); - /* prune entries by uid */ - for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1; - i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) { - ret = -2; /* acl_get_tag_type */ - if (acl_get_tag_type(entry, &tag_type) != 0) - goto out; - if (tag_type != ACL_USER) - continue; + if (perms == has_perms) { + ret = HAKUREI_ACL_DUP; + acl_d = acl_dup(acl); + if (acl_d == NULL) + goto out; - ret = -3; /* acl_get_qualifier */ - qualifier_p = acl_get_qualifier(entry); - if (qualifier_p == NULL) - goto out; - v = *(uid_t *)qualifier_p == uid; - acl_free(qualifier_p); + ret = HAKUREI_ACL_CALC_MASK; + if (acl_calc_mask(&acl_d) != 0) + goto out; - if (!v) - continue; + ret = HAKUREI_ACL_OK; + if (acl_cmp(acl, acl_d) == 0) + goto out; + } - ret = -4; /* acl_delete_entry */ - if (acl_delete_entry(acl, entry) != 0) - goto out; - } +delete: + ret = HAKUREI_ACL_DELETE_ENTRY; + if (acl_delete_entry(acl, entry) != 0) + goto out; + } - if (plen == 0) - goto set; + if (perms == 0) + goto set; - ret = -5; /* acl_create_entry */ - if (acl_create_entry(&acl, &entry) != 0) - goto out; + ret = HAKUREI_ACL_CREATE_ENTRY; + if (acl_create_entry(&acl, &entry) != 0) + goto out; - ret = -6; /* acl_get_permset */ - if (acl_get_permset(entry, &permset) != 0) - goto out; + ret = HAKUREI_ACL_GET_PERMSET; + if (acl_get_permset(entry, &permset) != 0) + goto out; - ret = -7; /* acl_add_perm */ - for (i = 0; i < plen; i++) { - if (acl_add_perm(permset, perms[i]) != 0) - goto out; - } + ret = HAKUREI_ACL_ADD_PERM; + if (acl_add_perm(permset, perms) != 0) + goto out; - ret = -8; /* acl_set_tag_type */ - if (acl_set_tag_type(entry, ACL_USER) != 0) - goto out; + ret = HAKUREI_ACL_SET_TAG_TYPE; + if (acl_set_tag_type(entry, ACL_USER) != 0) + goto out; - ret = -9; /* acl_set_qualifier */ - if (acl_set_qualifier(entry, (void *)&uid) != 0) - goto out; + ret = HAKUREI_ACL_SET_QUALIFIER; + if (acl_set_qualifier(entry, (void *)&uid) != 0) + goto out; set: - ret = -10; /* acl_calc_mask */ - if (acl_calc_mask(&acl) != 0) - goto out; + ret = HAKUREI_ACL_CALC_MASK; + if (acl_calc_mask(&acl) != 0) + goto out; - ret = -11; /* acl_valid */ - if (acl_valid(acl) != 0) - goto out; + ret = HAKUREI_ACL_VALID; + if (acl_valid(acl) != 0) + goto out; - ret = -12; /* acl_set_file */ - if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0) - ret = 0; + ret = HAKUREI_ACL_SET_FILE; + if (acl_set_file_at(fd, "", AT_EMPTY_PATH, ACL_TYPE_ACCESS, acl) == 0) + ret = HAKUREI_ACL_OK; out: - free((void *)path_p); - if (acl != NULL) - acl_free((void *)acl); - return ret; + if (acl != NULL) + acl_free((void *)acl); + if (acl_d != NULL) + acl_free((void *)acl_d); + return ret; } diff --git a/internal/acl/libacl-helper.go b/internal/acl/libacl-helper.go deleted file mode 100644 index 22575296..00000000 --- a/internal/acl/libacl-helper.go +++ /dev/null @@ -1,40 +0,0 @@ -package acl - -import "os" - -func newAclPathError(name string, r int, err error) error { - pathError := &os.PathError{Path: name, Err: err} - switch r { - case 0: - return nil - - case -1: - pathError.Op = "acl_get_file" - case -2: - pathError.Op = "acl_get_tag_type" - case -3: - pathError.Op = "acl_get_qualifier" - case -4: - pathError.Op = "acl_delete_entry" - case -5: - pathError.Op = "acl_create_entry" - case -6: - pathError.Op = "acl_get_permset" - case -7: - pathError.Op = "acl_add_perm" - case -8: - pathError.Op = "acl_set_tag_type" - case -9: - pathError.Op = "acl_set_qualifier" - case -10: - pathError.Op = "acl_calc_mask" - case -11: - pathError.Op = "acl_valid" - case -12: - pathError.Op = "acl_set_file" - - default: // unreachable - pathError.Op = "setfacl" - } - return pathError -} diff --git a/internal/acl/libacl-helper.h b/internal/acl/libacl-helper.h index b86eb170..111fac4a 100644 --- a/internal/acl/libacl-helper.h +++ b/internal/acl/libacl-helper.h @@ -1,4 +1,21 @@ #include <sys/acl.h> -int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid, - acl_perm_t *perms, size_t plen); +enum hakurei_acl_status { + HAKUREI_ACL_OK, + HAKUREI_ACL_GET_FILE, + HAKUREI_ACL_GET_TAG_TYPE, + HAKUREI_ACL_GET_QUALIFIER, + HAKUREI_ACL_DELETE_ENTRY, + HAKUREI_ACL_CREATE_ENTRY, + HAKUREI_ACL_GET_PERMSET, + HAKUREI_ACL_ADD_PERM, + HAKUREI_ACL_SET_TAG_TYPE, + HAKUREI_ACL_SET_QUALIFIER, + HAKUREI_ACL_CALC_MASK, + HAKUREI_ACL_VALID, + HAKUREI_ACL_SET_FILE, + HAKUREI_ACL_GET_PERM, + HAKUREI_ACL_DUP, +}; + +enum hakurei_acl_status hakurei_update_acl(int fd, uid_t uid, acl_perm_t perms); diff --git a/internal/acl/libacl-helper_test.go b/internal/acl/libacl-helper_test.go index 13d23a3c..05c43639 100644 --- a/internal/acl/libacl-helper_test.go +++ b/internal/acl/libacl-helper_test.go @@ -1,4 +1,4 @@ -package acl +package acl_test import ( "os" @@ -7,9 +7,12 @@ import ( "testing" "hakurei.app/container" + "hakurei.app/internal/acl" ) -func TestNewAclPathError(t *testing.T) { +func TestError(t *testing.T) { + t.Parallel() + testCases := []struct { name string path string @@ -17,43 +20,51 @@ func TestNewAclPathError(t *testing.T) { err error want error }{ - {"nil", container.Nonexistent, 0, syscall.ENOTRECOVERABLE, nil}, + {"nil", container.Nonexistent, 0, syscall.ENOTRECOVERABLE, os.ErrInvalid}, - {"acl_get_file", container.Nonexistent, -1, syscall.ENOTRECOVERABLE, + {"acl_get_file", container.Nonexistent, 1, syscall.ENOTRECOVERABLE, &os.PathError{Op: "acl_get_file", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_get_tag_type", container.Nonexistent, -2, syscall.ENOTRECOVERABLE, + {"acl_get_tag_type", container.Nonexistent, 2, syscall.ENOTRECOVERABLE, &os.PathError{Op: "acl_get_tag_type", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_get_qualifier", container.Nonexistent, -3, syscall.ENOTRECOVERABLE, + {"acl_get_qualifier", container.Nonexistent, 3, syscall.ENOTRECOVERABLE, &os.PathError{Op: "acl_get_qualifier", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_delete_entry", container.Nonexistent, -4, syscall.ENOTRECOVERABLE, + {"acl_delete_entry", container.Nonexistent, 4, syscall.ENOTRECOVERABLE, &os.PathError{Op: "acl_delete_entry", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_create_entry", container.Nonexistent, -5, syscall.ENOTRECOVERABLE, + {"acl_create_entry", container.Nonexistent, 5, syscall.ENOTRECOVERABLE, &os.PathError{Op: "acl_create_entry", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_get_permset", container.Nonexistent, -6, syscall.ENOTRECOVERABLE, + {"acl_get_permset", container.Nonexistent, 6, syscall.ENOTRECOVERABLE, &os.PathError{Op: "acl_get_permset", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_add_perm", container.Nonexistent, -7, syscall.ENOTRECOVERABLE, + {"acl_add_perm", container.Nonexistent, 7, syscall.ENOTRECOVERABLE, &os.PathError{Op: "acl_add_perm", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_set_tag_type", container.Nonexistent, -8, syscall.ENOTRECOVERABLE, + {"acl_set_tag_type", container.Nonexistent, 8, syscall.ENOTRECOVERABLE, &os.PathError{Op: "acl_set_tag_type", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_set_qualifier", container.Nonexistent, -9, syscall.ENOTRECOVERABLE, + {"acl_set_qualifier", container.Nonexistent, 9, syscall.ENOTRECOVERABLE, &os.PathError{Op: "acl_set_qualifier", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_calc_mask", container.Nonexistent, -10, syscall.ENOTRECOVERABLE, + {"acl_calc_mask", container.Nonexistent, 10, syscall.ENOTRECOVERABLE, &os.PathError{Op: "acl_calc_mask", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_valid", container.Nonexistent, -11, syscall.ENOTRECOVERABLE, + {"acl_valid", container.Nonexistent, 11, syscall.ENOTRECOVERABLE, &os.PathError{Op: "acl_valid", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_set_file", container.Nonexistent, -12, syscall.ENOTRECOVERABLE, + {"acl_set_file", container.Nonexistent, 12, syscall.ENOTRECOVERABLE, &os.PathError{Op: "acl_set_file", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, + {"acl_get_perm", container.Nonexistent, 13, syscall.ENOTRECOVERABLE, + &os.PathError{Op: "acl_get_perm", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, + {"acl_dup", container.Nonexistent, 14, syscall.ENOTRECOVERABLE, + &os.PathError{Op: "acl_dup", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl", container.Nonexistent, -13, syscall.ENOTRECOVERABLE, + {"acl", container.Nonexistent, 15, syscall.ENOTRECOVERABLE, &os.PathError{Op: "setfacl", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, {"invalid", container.Nonexistent, -0xdead, nil, &os.PathError{Op: "setfacl", Path: container.Nonexistent}}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - err := newAclPathError(tc.path, tc.r, tc.err) + err := acl.Error{ + Path: tc.path, + Op: acl.Status(tc.r), + Err: tc.err, + }.Unwrap() if !reflect.DeepEqual(err, tc.want) { - t.Errorf("newAclPathError: %v, want %v", err, tc.want) + t.Errorf("Unwrap: %v, want %v", err, tc.want) } }) } diff --git a/internal/acl/perms.go b/internal/acl/perms.go index fbc99e82..829b134f 100644 --- a/internal/acl/perms.go +++ b/internal/acl/perms.go @@ -1,18 +1,18 @@ package acl -type Perms []Perm +import "unsafe" -func (ps Perms) String() string { - var s = []byte("---") - for _, p := range ps { - switch p { - case Read: - s[0] = 'r' - case Write: - s[1] = 'w' - case Execute: - s[2] = 'x' - } +// String returns a text representation of perms. +func (perms Perm) String() string { + buf := []byte{'-', '-', '-'} + if perms&Read != 0 { + buf[0] = 'r' } - return string(s) + if perms&Write != 0 { + buf[1] = 'w' + } + if perms&Execute != 0 { + buf[2] = 'x' + } + return unsafe.String(unsafe.SliceData(buf), len(buf)) } diff --git a/internal/acl/perms_test.go b/internal/acl/perms_test.go index 3920046d..9fa24dc8 100644 --- a/internal/acl/perms_test.go +++ b/internal/acl/perms_test.go @@ -7,18 +7,20 @@ import ( ) func TestPerms(t *testing.T) { + t.Parallel() + testCases := []struct { name string - perms acl.Perms + perms acl.Perm }{ - {"---", acl.Perms{}}, - {"r--", acl.Perms{acl.Read}}, - {"-w-", acl.Perms{acl.Write}}, - {"--x", acl.Perms{acl.Execute}}, - {"rw-", acl.Perms{acl.Read, acl.Read, acl.Write}}, - {"r-x", acl.Perms{acl.Read, acl.Execute, acl.Execute}}, - {"-wx", acl.Perms{acl.Write, acl.Write, acl.Execute, acl.Execute}}, - {"rwx", acl.Perms{acl.Read, acl.Write, acl.Execute}}, + {"---", 0}, + {"r--", acl.Read}, + {"-w-", acl.Write}, + {"--x", acl.Execute}, + {"rw-", acl.Read | acl.Write}, + {"r-x", acl.Read | acl.Execute}, + {"-wx", acl.Write | acl.Execute}, + {"rwx", acl.Read | acl.Write | acl.Execute}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/lockedfile/lockedfile.go b/internal/lockedfile/lockedfile.go index f48124ff..c7baff60 100644 --- a/internal/lockedfile/lockedfile.go +++ b/internal/lockedfile/lockedfile.go @@ -34,15 +34,12 @@ type osFile struct { *os.File } -// OpenFile is like os.OpenFile, but returns a locked file. -// If flag includes os.O_WRONLY or os.O_RDWR, the file is write-locked; -// otherwise, it is read-locked. -func OpenFile(name string, flag int, perm fs.FileMode) (*File, error) { +func openFile(name string, flag int, perm fs.FileMode, w bool) (*File, error) { var ( f = new(File) err error ) - f.osFile.File, err = openFile(name, flag, perm) + f.osFile.File, err = osOpenFile(name, flag, perm, w) if err != nil { return nil, err } @@ -59,11 +56,23 @@ func OpenFile(name string, flag int, perm fs.FileMode) (*File, error) { return f, nil } +// OpenFile is like os.OpenFile, but returns a locked file. +// If flag includes os.O_WRONLY or os.O_RDWR, the file is write-locked; +// otherwise, it is read-locked. +func OpenFile(name string, flag int, perm fs.FileMode) (*File, error) { + return openFile(name, flag, perm, false) +} + // Open is like os.Open, but returns a read-locked file. func Open(name string) (*File, error) { return OpenFile(name, os.O_RDONLY, 0) } +// OpenDir is like Open, but returns a write-locked directory. +func OpenDir(name string) (*File, error) { + return openFile(name, os.O_RDONLY, 0, true) +} + // Create is like os.Create, but returns a write-locked file. func Create(name string) (*File, error) { return OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) diff --git a/internal/lockedfile/lockedfile_filelock.go b/internal/lockedfile/lockedfile_filelock.go index 9fc16a9b..1f5ca310 100644 --- a/internal/lockedfile/lockedfile_filelock.go +++ b/internal/lockedfile/lockedfile_filelock.go @@ -13,7 +13,7 @@ import ( "hakurei.app/internal/lockedfile/internal/filelock" ) -func openFile(name string, flag int, perm fs.FileMode) (*os.File, error) { +func osOpenFile(name string, flag int, perm fs.FileMode, w bool) (*os.File, error) { // On BSD systems, we could add the O_SHLOCK or O_EXLOCK flag to the OpenFile // call instead of locking separately, but we have to support separate locking // calls for Linux and Windows anyway, so it's simpler to use that approach @@ -24,6 +24,10 @@ func openFile(name string, flag int, perm fs.FileMode) (*os.File, error) { return nil, err } + if w { + flag |= os.O_WRONLY + } + switch flag & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR) { case os.O_WRONLY, os.O_RDWR: err = filelock.Lock(f) diff --git a/internal/lockedfile/lockedfile_plan9.go b/internal/lockedfile/lockedfile_plan9.go index a2ce794b..45e2b87c 100644 --- a/internal/lockedfile/lockedfile_plan9.go +++ b/internal/lockedfile/lockedfile_plan9.go @@ -42,7 +42,7 @@ func isLocked(err error) bool { return false } -func openFile(name string, flag int, perm fs.FileMode) (*os.File, error) { +func osOpenFile(name string, flag int, perm fs.FileMode, _ bool) (*os.File, error) { // Plan 9 uses a mode bit instead of explicit lock/unlock syscalls. // // Per http://man.cat-v.org/plan_9/5/stat: “Exclusive use files may be open diff --git a/internal/outcome/dispatcher_test.go b/internal/outcome/dispatcher_test.go index 00771e56..3f4732b5 100644 --- a/internal/outcome/dispatcher_test.go +++ b/internal/outcome/dispatcher_test.go @@ -209,7 +209,7 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) { if err := s.populateLocal(k, k); err != nil { t.Fatalf("populateLocal: error = %v", err) } - stateSys := s.newSys(config, system.New(panicMsgContext{}, k, checkExpectUid)) + stateSys := s.newSys(config, system.New(panicMsgContext{}, k, checkExpectUid, nil)) if tc.pStateSys != nil { tc.pStateSys(stateSys) } @@ -279,7 +279,7 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) { } } -func newI() *system.I { return system.New(panicMsgContext{}, panicMsgContext{}, checkExpectUid) } +func newI() *system.I { return system.New(panicMsgContext{}, panicMsgContext{}, checkExpectUid, nil) } // simpleTestCase is a simple freeform test case utilising kstub. type simpleTestCase struct { diff --git a/internal/outcome/finalise.go b/internal/outcome/finalise.go index 4049b8d3..7e5faab6 100644 --- a/internal/outcome/finalise.go +++ b/internal/outcome/finalise.go @@ -74,7 +74,7 @@ func (k *outcome) finalise( return err } - sys := system.New(k.ctx, msg, s.uid.unwrap()) + sys := system.New(k.ctx, msg, s.uid.unwrap(), s.sc.SharePath.Append("lock")) if err := s.newSys(config, sys).toSystem(); err != nil { return err } diff --git a/internal/outcome/outcome.go b/internal/outcome/outcome.go index 0fe0016e..19ca9c7b 100644 --- a/internal/outcome/outcome.go +++ b/internal/outcome/outcome.go @@ -6,6 +6,7 @@ import ( "errors" "maps" "strconv" + "time" "hakurei.app/check" "hakurei.app/container" @@ -125,6 +126,14 @@ func newOutcomeState(k syscallDispatcher, msg message.Msg, id *hst.ID, config *h s.Shim.WaitDelay = s.Container.WaitDelay } + if s.Container.AdoptWaitDelay > 5*time.Second { + s.Shim.AdoptWaitDelay = 5 * time.Second + } else if s.Container.AdoptWaitDelay < 0 { + s.Shim.AdoptWaitDelay = -1 + } else { + s.Shim.AdoptWaitDelay = s.Container.AdoptWaitDelay + } + if s.Container.Flags&hst.FMapRealUID != 0 { s.Mapuid, s.Mapgid = k.getuid(), k.getgid() } else { @@ -240,8 +249,8 @@ func (state *outcomeStateSys) ensureRuntimeDir() { state.useRuntimeDir = true state.sys. // ensure this dir in case XDG_RUNTIME_DIR is unset - Ensure(state.sc.RuntimePath, 0700).UpdatePermType(system.User, state.sc.RuntimePath, acl.Execute). - Ensure(state.sc.RunDirPath, 0700).UpdatePermType(system.User, state.sc.RunDirPath, acl.Execute) + Ensure(state.sc.RuntimePath, 0700, true).UpdatePermType(system.User, state.sc.RuntimePath, acl.Execute). + Ensure(state.sc.RunDirPath, 0700, true).UpdatePermType(system.User, state.sc.RunDirPath, acl.Execute) } // instance returns the pathname to a process-specific directory within TMPDIR. @@ -252,7 +261,7 @@ func (state *outcomeStateSys) instance() *check.Absolute { return state.sharePath } state.sharePath = state.instancePath() - state.sys.Ephemeral(system.Process, state.sharePath, 0711) + state.sys.Ephemeral(system.Process, state.sharePath, 0711, false) return state.sharePath } @@ -265,7 +274,7 @@ func (state *outcomeStateSys) runtime() *check.Absolute { } state.ensureRuntimeDir() state.runtimeSharePath = state.runtimePath() - state.sys.Ephemeral(system.Process, state.runtimeSharePath, 0700) + state.sys.Ephemeral(system.Process, state.runtimeSharePath, 0700, true) state.sys.UpdatePerm(state.runtimeSharePath, acl.Execute) return state.runtimeSharePath } diff --git a/internal/outcome/run_test.go b/internal/outcome/run_test.go index a1d83ec9..d9a41f53 100644 --- a/internal/outcome/run_test.go +++ b/internal/outcome/run_test.go @@ -40,24 +40,22 @@ func TestOutcomeRun(t *testing.T) { wantSys *system.I wantParams *container.Params }{ - {"template", new(stubNixOS), hst.Template(), checkExpectInstanceId, system.New(panicMsgContext{}, message.New(nil), 10009). + {"template", new(stubNixOS), hst.Template(), checkExpectInstanceId, system.New(panicMsgContext{}, message.New(nil), 10009, nil). // spParamsOp - Ensure(m("/tmp/hakurei.0"), 0711). + Ensure(m("/tmp/hakurei.0"), 0711, false). // spRuntimeOp - Ensure(m("/tmp/hakurei.0/runtime"), 0700). - UpdatePermType(system.User, m("/tmp/hakurei.0/runtime"), acl.Execute). - Ensure(m("/tmp/hakurei.0/runtime/9"), 0700). - UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute). + Ensure(m("/tmp/hakurei.0/runtime"), 0711, false). + Ensure(m("/tmp/hakurei.0/runtime/9"), 0700, true). + UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/9"), acl.Read|acl.Write|acl.Execute). // spTmpdirOp - Ensure(m("/tmp/hakurei.0/tmpdir"), 0700). - UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir"), acl.Execute). - Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700). - UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute). + Ensure(m("/tmp/hakurei.0/tmpdir"), 0711, false). + Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700, true). + UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read|acl.Write|acl.Execute). // instance - Ephemeral(system.Process, m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0711). + Ephemeral(system.Process, m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0711, false). // spWaylandOp Wayland( @@ -84,13 +82,13 @@ func TestOutcomeRun(t *testing.T) { "unix:path=/var/run/dbus/system_bus_socket", "/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/system_bus_socket", }, - ).UpdatePerm(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus"), acl.Read, acl.Write). - UpdatePerm(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/system_bus_socket"), acl.Read, acl.Write). + ).UpdatePerm(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/bus"), acl.Read|acl.Write). + UpdatePerm(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/system_bus_socket"), acl.Read|acl.Write). // spFilesystemOp - Ensure(m("/var/lib/hakurei/u0"), 0700). + Ensure(m("/var/lib/hakurei/u0"), 0700, true). UpdatePermType(system.User, m("/var/lib/hakurei/u0"), acl.Execute). - UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"), acl.Read, acl.Write, acl.Execute), &container.Params{ + UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"), acl.Read|acl.Write|acl.Execute), &container.Params{ Dir: m("/data/data/org.chromium.Chromium"), Env: []string{ @@ -200,16 +198,14 @@ func TestOutcomeRun(t *testing.T) { 0x96, 0xd7, 0xbc, 0x15, 0xbd, 0x01, 0x78, 0x0e, 0xb9, 0xa6, 0x07, 0xac, - }, system.New(t.Context(), msg, 10000). - Ensure(m("/tmp/hakurei.0"), 0711). - Ensure(m("/tmp/hakurei.0/runtime"), 0700). - UpdatePermType(system.User, m("/tmp/hakurei.0/runtime"), acl.Execute). - Ensure(m("/tmp/hakurei.0/runtime/0"), 0700). - UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/0"), acl.Read, acl.Write, acl.Execute). - Ensure(m("/tmp/hakurei.0/tmpdir"), 0700). - UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir"), acl.Execute). - Ensure(m("/tmp/hakurei.0/tmpdir/0"), 01700). - UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/0"), acl.Read, acl.Write, acl.Execute), &container.Params{ + }, system.New(t.Context(), msg, 10000, nil). + Ensure(m("/tmp/hakurei.0"), 0711, false). + Ensure(m("/tmp/hakurei.0/runtime"), 0711, false). + Ensure(m("/tmp/hakurei.0/runtime/0"), 0700, true). + UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/0"), acl.Read|acl.Write|acl.Execute). + Ensure(m("/tmp/hakurei.0/tmpdir"), 0711, false). + Ensure(m("/tmp/hakurei.0/tmpdir/0"), 01700, true). + UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/0"), acl.Read|acl.Write|acl.Execute), &container.Params{ Dir: m("/home/chronos"), Path: m("/run/current-system/sw/bin/zsh"), @@ -326,13 +322,13 @@ func TestOutcomeRun(t *testing.T) { 0xb1, 0x75, 0x91, 0x17, 0x82, 0xd4, 0x13, 0x36, 0x9b, 0x64, 0xce, 0x7c, - }, system.New(t.Context(), msg, 10009). - Ensure(m("/tmp/hakurei.0"), 0711). - Ensure(m("/tmp/hakurei.0/runtime"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime"), acl.Execute). - Ensure(m("/tmp/hakurei.0/runtime/9"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute). - Ensure(m("/tmp/hakurei.0/tmpdir"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir"), acl.Execute). - Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute). - Ephemeral(system.Process, m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c"), 0711). + }, system.New(t.Context(), msg, 10009, nil). + Ensure(m("/tmp/hakurei.0"), 0711, false). + Ensure(m("/tmp/hakurei.0/runtime"), 0711, false). + Ensure(m("/tmp/hakurei.0/runtime/9"), 0700, true).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/9"), acl.Read|acl.Write|acl.Execute). + Ensure(m("/tmp/hakurei.0/tmpdir"), 0711, false). + Ensure(m("/tmp/hakurei.0/tmpdir/9"), 01700, true).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/9"), acl.Read|acl.Write|acl.Execute). + Ephemeral(system.Process, m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c"), 0711, false). Wayland(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"). PipeWire(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/pipewire"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"). MustProxyDBus(&hst.BusConfig{ @@ -371,8 +367,8 @@ func TestOutcomeRun(t *testing.T) { "unix:path=/var/run/dbus/system_bus_socket", "/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket", }). - UpdatePerm(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), acl.Read, acl.Write). - UpdatePerm(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), acl.Read, acl.Write), &container.Params{ + UpdatePerm(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/bus"), acl.Read|acl.Write). + UpdatePerm(m("/tmp/hakurei.0/ebf083d1b175911782d413369b64ce7c/system_bus_socket"), acl.Read|acl.Write), &container.Params{ Dir: m("/home/chronos"), Path: m("/run/current-system/sw/bin/zsh"), @@ -477,16 +473,16 @@ func TestOutcomeRun(t *testing.T) { 0x66, 0xda, 0xbe, 0x57, 0x4c, 0xf0, 0x73, 0xbd, 0xb4, 0x6e, 0xb5, 0xc1, - }, system.New(t.Context(), msg, 10001). - Ensure(m("/tmp/hakurei.0"), 0711). - Ensure(m("/tmp/hakurei.0/runtime"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime"), acl.Execute). - Ensure(m("/tmp/hakurei.0/runtime/1"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/1"), acl.Read, acl.Write, acl.Execute). - Ensure(m("/tmp/hakurei.0/tmpdir"), 0700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir"), acl.Execute). - Ensure(m("/tmp/hakurei.0/tmpdir/1"), 01700).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/1"), acl.Read, acl.Write, acl.Execute). - Ensure(m("/run/user/1971"), 0700).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset - Ensure(m("/run/user/1971/hakurei"), 0700).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute). - UpdatePermType(hst.EWayland, m("/run/user/1971/wayland-0"), acl.Read, acl.Write, acl.Execute). - Ephemeral(system.Process, m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1"), 0711). + }, system.New(t.Context(), msg, 10001, nil). + Ensure(m("/tmp/hakurei.0"), 0711, false). + Ensure(m("/tmp/hakurei.0/runtime"), 0711, false). + Ensure(m("/tmp/hakurei.0/runtime/1"), 0700, true).UpdatePermType(system.User, m("/tmp/hakurei.0/runtime/1"), acl.Read|acl.Write|acl.Execute). + Ensure(m("/tmp/hakurei.0/tmpdir"), 0711, false). + Ensure(m("/tmp/hakurei.0/tmpdir/1"), 01700, true).UpdatePermType(system.User, m("/tmp/hakurei.0/tmpdir/1"), acl.Read|acl.Write|acl.Execute). + Ensure(m("/run/user/1971"), 0700, true).UpdatePermType(system.User, m("/run/user/1971"), acl.Execute). // this is ordered as is because the previous Ensure only calls mkdir if XDG_RUNTIME_DIR is unset + Ensure(m("/run/user/1971/hakurei"), 0700, true).UpdatePermType(system.User, m("/run/user/1971/hakurei"), acl.Execute). + UpdatePermType(hst.EWayland, m("/run/user/1971/wayland-0"), acl.Read|acl.Write|acl.Execute). + Ephemeral(system.Process, m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1"), 0711, false). PipeWire(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/pipewire"), "org.chromium.Chromium", "8e2c76b066dabe574cf073bdb46eb5c1"). MustProxyDBus(&hst.BusConfig{ Talk: []string{ @@ -515,8 +511,8 @@ func TestOutcomeRun(t *testing.T) { "unix:path=/var/run/dbus/system_bus_socket", "/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket", }). - UpdatePerm(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), acl.Read, acl.Write). - UpdatePerm(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), acl.Read, acl.Write), &container.Params{ + UpdatePerm(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), acl.Read|acl.Write). + UpdatePerm(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/system_bus_socket"), acl.Read|acl.Write), &container.Params{ Uid: 1971, Gid: 100, @@ -581,7 +577,7 @@ func TestOutcomeRun(t *testing.T) { t.Fatalf("populateLocal: error = %#v", err) } - gotSys = system.New(t.Context(), msg, sPriv.uid.unwrap()) + gotSys = system.New(t.Context(), msg, sPriv.uid.unwrap(), nil) if err := sPriv.newSys(tc.config, gotSys).toSystem(); err != nil { t.Fatalf("toSystem: error = %#v", err) } diff --git a/internal/outcome/shim-signal.c b/internal/outcome/shim-signal.c index f02c0df1..6543d4b8 100644 --- a/internal/outcome/shim-signal.c +++ b/internal/outcome/shim-signal.c @@ -9,54 +9,57 @@ static pid_t hakurei_shim_param_ppid = -1; static int hakurei_shim_fd = -1; /* see shim.go for handling of the message */ -static inline ssize_t hakurei_shim_write(hakurei_shim_msg msg) { - int savedErrno = errno; - unsigned char buf = (unsigned char)msg; - ssize_t ret = write(hakurei_shim_fd, &buf, 1); - if (ret == -1 && errno != EAGAIN) - exit(EXIT_FAILURE); - errno = savedErrno; - return ret; +static inline ssize_t hakurei_shim_write(hakurei_shim_msg msg) +{ + int savedErrno = errno; + unsigned char buf = (unsigned char)msg; + ssize_t ret = write(hakurei_shim_fd, &buf, 1); + if (ret == -1 && errno != EAGAIN) + exit(EXIT_FAILURE); + errno = savedErrno; + return ret; } -static void hakurei_shim_sigaction(int sig, siginfo_t *si, void *ucontext) { - if (sig != SIGCONT || si == NULL) { - hakurei_shim_write(HAKUREI_SHIM_INVALID); - return; - } +static void hakurei_shim_sigaction(int sig, siginfo_t *si, void *ucontext) +{ + if (sig != SIGCONT || si == NULL) { + hakurei_shim_write(HAKUREI_SHIM_INVALID); + return; + } - if (si->si_pid == hakurei_shim_param_ppid) { - hakurei_shim_write(HAKUREI_SHIM_EXIT_REQUESTED); - return; - } + if (si->si_pid == hakurei_shim_param_ppid) { + hakurei_shim_write(HAKUREI_SHIM_EXIT_REQUESTED); + return; + } - hakurei_shim_write(HAKUREI_SHIM_BAD_PID); + hakurei_shim_write(HAKUREI_SHIM_BAD_PID); - if (getppid() != hakurei_shim_param_ppid) - hakurei_shim_write(HAKUREI_SHIM_ORPHAN); + if (getppid() != hakurei_shim_param_ppid) + hakurei_shim_write(HAKUREI_SHIM_ORPHAN); } -void hakurei_shim_setup_cont_signal(pid_t ppid, int fd) { - if (hakurei_shim_param_ppid != -1 || hakurei_shim_fd != -1) - *(volatile int *)NULL = 0; /* unreachable */ +void hakurei_shim_setup_cont_signal(pid_t ppid, int fd) +{ + if (hakurei_shim_param_ppid != -1 || hakurei_shim_fd != -1) + *(volatile int *)NULL = 0; /* unreachable */ - struct sigaction new_action = {0}, old_action = {0}; - if (sigaction(SIGCONT, NULL, &old_action) != 0) - return; - if (old_action.sa_handler != SIG_DFL) { - errno = ENOTRECOVERABLE; - return; - } + struct sigaction new_action = { 0 }, old_action = { 0 }; + if (sigaction(SIGCONT, NULL, &old_action) != 0) + return; + if (old_action.sa_handler != SIG_DFL) { + errno = ENOTRECOVERABLE; + return; + } - new_action.sa_sigaction = hakurei_shim_sigaction; - if (sigemptyset(&new_action.sa_mask) != 0) - return; - new_action.sa_flags = SA_ONSTACK | SA_SIGINFO; + new_action.sa_sigaction = hakurei_shim_sigaction; + if (sigemptyset(&new_action.sa_mask) != 0) + return; + new_action.sa_flags = SA_ONSTACK | SA_SIGINFO; - if (sigaction(SIGCONT, &new_action, NULL) != 0) - return; + if (sigaction(SIGCONT, &new_action, NULL) != 0) + return; - errno = 0; - hakurei_shim_param_ppid = ppid; - hakurei_shim_fd = fd; + errno = 0; + hakurei_shim_param_ppid = ppid; + hakurei_shim_fd = fd; } diff --git a/internal/outcome/shim-signal.h b/internal/outcome/shim-signal.h index f396a915..db43b561 100644 --- a/internal/outcome/shim-signal.h +++ b/internal/outcome/shim-signal.h @@ -2,10 +2,10 @@ /* see shim.go for documentation */ typedef enum { - HAKUREI_SHIM_EXIT_REQUESTED, - HAKUREI_SHIM_ORPHAN, - HAKUREI_SHIM_INVALID, - HAKUREI_SHIM_BAD_PID, + HAKUREI_SHIM_EXIT_REQUESTED, + HAKUREI_SHIM_ORPHAN, + HAKUREI_SHIM_INVALID, + HAKUREI_SHIM_BAD_PID, } hakurei_shim_msg; void hakurei_shim_setup_cont_signal(pid_t ppid, int fd); diff --git a/internal/outcome/shim.go b/internal/outcome/shim.go index 6e039b65..2e7b0910 100644 --- a/internal/outcome/shim.go +++ b/internal/outcome/shim.go @@ -72,6 +72,12 @@ type shimParams struct { // Limits are enforced on the priv side. WaitDelay time.Duration + // Duration to wait after the initial process terminates before all + // lingering processes are killed. + // + // Limits are enforced on the priv side. + AdoptWaitDelay time.Duration + // Verbosity pass through from [message.Msg]. Verbose bool @@ -286,6 +292,7 @@ func shimEntrypoint(k syscallDispatcher) { // bounds and default enforced in finalise.go z.WaitDelay = state.Shim.WaitDelay + z.AdoptWaitDelay = state.Shim.AdoptWaitDelay if stateParams.pipewirePulsePath != nil { zpw := container.NewCommand(ctx, msg, stateParams.pipewirePulsePath, pipewirePulseName) diff --git a/internal/outcome/spcontainer.go b/internal/outcome/spcontainer.go index bfa5dbdc..4ba3bb2d 100644 --- a/internal/outcome/spcontainer.go +++ b/internal/outcome/spcontainer.go @@ -39,7 +39,7 @@ type spParamsOp struct { func (s *spParamsOp) toSystem(state *outcomeStateSys) error { s.Term, s.TermSet = state.k.lookupEnv("TERM") - state.sys.Ensure(state.sc.SharePath, 0711) + state.sys.Ensure(state.sc.SharePath, 0711, false) return nil } @@ -351,20 +351,20 @@ func flattenExtraPerms(sys *system.I, extraPerms []hst.ExtraPermConfig) { } if p.Ensure { - sys.Ensure(p.Path, 0700) + sys.Ensure(p.Path, 0700, true) } - perms := make(acl.Perms, 0, 3) + var perms acl.Perm if p.Read { - perms = append(perms, acl.Read) + perms |= acl.Read } if p.Write { - perms = append(perms, acl.Write) + perms |= acl.Write } if p.Execute { - perms = append(perms, acl.Execute) + perms |= acl.Execute } - sys.UpdatePermType(system.User, p.Path, perms...) + sys.UpdatePermType(system.User, p.Path, perms) } } diff --git a/internal/outcome/spcontainer_test.go b/internal/outcome/spcontainer_test.go index 7dd47735..54c276f9 100644 --- a/internal/outcome/spcontainer_test.go +++ b/internal/outcome/spcontainer_test.go @@ -36,7 +36,7 @@ func TestSpParamsOp(t *testing.T) { }, nil, []stub.Call{ call("lookupEnv", stub.ExpectArgs{"TERM"}, "xterm", nil), }, newI(). - Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711), nil, nil, nil, []stub.Call{ + Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711, false), nil, nil, nil, []stub.Call{ // this op configures the container state and does not make calls during toContainer }, nil, nil, &hst.AppError{ Step: "finalise", @@ -57,7 +57,7 @@ func TestSpParamsOp(t *testing.T) { }, nil, []stub.Call{ call("lookupEnv", stub.ExpectArgs{"TERM"}, "xterm", nil), }, newI(). - Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711), nil, nil, nil, []stub.Call{ + Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711, false), nil, nil, nil, []stub.Call{ // this op configures the container state and does not make calls during toContainer }, &container.Params{ Hostname: config.Container.Hostname, @@ -94,7 +94,7 @@ func TestSpParamsOp(t *testing.T) { }, hst.Template, nil, []stub.Call{ call("lookupEnv", stub.ExpectArgs{"TERM"}, "xterm", nil), }, newI(). - Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711), nil, nil, nil, []stub.Call{ + Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711, false), nil, nil, nil, []stub.Call{ // this op configures the container state and does not make calls during toContainer }, &container.Params{ Hostname: config.Container.Hostname, @@ -309,11 +309,11 @@ func TestSpFilesystemOp(t *testing.T) { call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium"}, nePrefix+"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium", nil), call("verbosef", stub.ExpectArgs{"hiding path %q from %q", []any{"/proc/nonexistent/eval/etc/dbus", "/etc/"}}, nil, nil), }, newI(). - Ensure(m("/var/lib/hakurei/u0"), 0700). + Ensure(m("/var/lib/hakurei/u0"), 0700, true). UpdatePermType(system.User, m("/var/lib/hakurei/u0"), acl.Execute). UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"), - acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) { + acl.Read|acl.Write|acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) { state.filesystem = append(configSmall.Container.Filesystem, hst.FilesystemConfigJSON{}) })), []stub.Call{ // this op configures the container state and does not make calls during toContainer @@ -342,11 +342,11 @@ func TestSpFilesystemOp(t *testing.T) { call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium"}, nePrefix+"/var/lib/hakurei/base/org.nixos/org.chromium.Chromium", nil), call("verbosef", stub.ExpectArgs{"hiding path %q from %q", []any{"/proc/nonexistent/eval/etc/dbus", "/etc/"}}, nil, nil), }, newI(). - Ensure(m("/var/lib/hakurei/u0"), 0700). + Ensure(m("/var/lib/hakurei/u0"), 0700, true). UpdatePermType(system.User, m("/var/lib/hakurei/u0"), acl.Execute). UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"), - acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) { + acl.Read|acl.Write|acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) { state.filesystem = configSmall.Container.Filesystem })), []stub.Call{ // this op configures the container state and does not make calls during toContainer @@ -397,11 +397,11 @@ func TestSpFilesystemOp(t *testing.T) { call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/usr"}, nePrefix+"/var/lib/hakurei/base/org.debian/usr", nil), call("evalSymlinks", stub.ExpectArgs{"/var/lib/hakurei/base/org.debian/var"}, nePrefix+"/var/lib/hakurei/base/org.debian/var", nil), }, newI(). - Ensure(m("/var/lib/hakurei/u0"), 0700). + Ensure(m("/var/lib/hakurei/u0"), 0700, true). UpdatePermType(system.User, m("/var/lib/hakurei/u0"), acl.Execute). UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"), - acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) { + acl.Read|acl.Write|acl.Execute), nil, nil, insertsOps(needsApplyState(func(state *outcomeStateParams) { state.filesystem = config.Container.Filesystem[1:] })), []stub.Call{ // this op configures the container state and does not make calls during toContainer @@ -441,18 +441,18 @@ func TestFlattenExtraPerms(t *testing.T) { want *system.I }{ {"path nil check", append(hst.Template().ExtraPerms, hst.ExtraPermConfig{}), newI(). - Ensure(m("/var/lib/hakurei/u0"), 0700). + Ensure(m("/var/lib/hakurei/u0"), 0700, true). UpdatePermType(system.User, m("/var/lib/hakurei/u0"), acl.Execute). UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"), - acl.Read, acl.Write, acl.Execute)}, + acl.Read|acl.Write|acl.Execute)}, {"template", hst.Template().ExtraPerms, newI(). - Ensure(m("/var/lib/hakurei/u0"), 0700). + Ensure(m("/var/lib/hakurei/u0"), 0700, true). UpdatePermType(system.User, m("/var/lib/hakurei/u0"), acl.Execute). UpdatePermType(system.User, m("/var/lib/hakurei/u0/org.chromium.Chromium"), - acl.Read, acl.Write, acl.Execute)}, + acl.Read|acl.Write|acl.Execute)}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/outcome/spdbus.go b/internal/outcome/spdbus.go index a50f9cd2..c45dde6c 100644 --- a/internal/outcome/spdbus.go +++ b/internal/outcome/spdbus.go @@ -38,10 +38,10 @@ func (s *spDBusOp) toSystem(state *outcomeStateSys) error { return err } - state.sys.UpdatePerm(sessionPath, acl.Read, acl.Write) + state.sys.UpdatePerm(sessionPath, acl.Read|acl.Write) if state.systemBus != nil { s.ProxySystem = true - state.sys.UpdatePerm(systemPath, acl.Read, acl.Write) + state.sys.UpdatePerm(systemPath, acl.Read|acl.Write) } return nil } diff --git a/internal/outcome/spdbus_test.go b/internal/outcome/spdbus_test.go index 6f357193..3b9f07f7 100644 --- a/internal/outcome/spdbus_test.go +++ b/internal/outcome/spdbus_test.go @@ -80,8 +80,8 @@ func TestSpDBusOp(t *testing.T) { "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*", )}}, nil, nil), }, func() *system.I { - sys := system.New(panicMsgContext{}, message.New(nil), checkExpectUid) - sys.Ephemeral(system.Process, m(wantInstancePrefix), 0711) + sys := system.New(panicMsgContext{}, message.New(nil), checkExpectUid, nil) + sys.Ephemeral(system.Process, m(wantInstancePrefix), 0711, false) if err := sys.ProxyDBus( dbus.NewConfig(config.ID, true, true), nil, dbus.ProxyPair{"unix:path=/run/user/1000/bus", wantInstancePrefix + "/bus"}, @@ -89,7 +89,7 @@ func TestSpDBusOp(t *testing.T) { ); err != nil { t.Fatalf("cannot prepare sys: %v", err) } - sys.UpdatePerm(m(wantInstancePrefix+"/bus"), acl.Read, acl.Write) + sys.UpdatePerm(m(wantInstancePrefix+"/bus"), acl.Read|acl.Write) return sys }(), sysUsesInstance(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{ // this op configures the container state and does not make calls during toContainer @@ -162,8 +162,8 @@ func TestSpDBusOp(t *testing.T) { "--talk=org.freedesktop.UPower", )}}, nil, nil), }, func() *system.I { - sys := system.New(panicMsgContext{}, message.New(nil), checkExpectUid) - sys.Ephemeral(system.Process, m(wantInstancePrefix), 0711) + sys := system.New(panicMsgContext{}, message.New(nil), checkExpectUid, nil) + sys.Ephemeral(system.Process, m(wantInstancePrefix), 0711, false) if err := sys.ProxyDBus( config.SessionBus, config.SystemBus, dbus.ProxyPair{"unix:path=/run/user/1000/bus", wantInstancePrefix + "/bus"}, @@ -171,8 +171,8 @@ func TestSpDBusOp(t *testing.T) { ); err != nil { t.Fatalf("cannot prepare sys: %v", err) } - sys.UpdatePerm(m(wantInstancePrefix+"/bus"), acl.Read, acl.Write). - UpdatePerm(m(wantInstancePrefix+"/system_bus_socket"), acl.Read, acl.Write) + sys.UpdatePerm(m(wantInstancePrefix+"/bus"), acl.Read|acl.Write). + UpdatePerm(m(wantInstancePrefix+"/system_bus_socket"), acl.Read|acl.Write) return sys }(), sysUsesInstance(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{ // this op configures the container state and does not make calls during toContainer diff --git a/internal/outcome/sppipewire_test.go b/internal/outcome/sppipewire_test.go index 1e3dab6a..804404dc 100644 --- a/internal/outcome/sppipewire_test.go +++ b/internal/outcome/sppipewire_test.go @@ -31,7 +31,7 @@ func TestSpPipeWireOp(t *testing.T) { return c }, nil, []stub.Call{}, newI(). // state.instance - Ephemeral(system.Process, m(wantInstancePrefix), 0711). + Ephemeral(system.Process, m(wantInstancePrefix), 0711, false). // toSystem PipeWire( m(wantInstancePrefix+"/pipewire"), diff --git a/internal/outcome/sppulse_test.go b/internal/outcome/sppulse_test.go index a1ff0d03..a9995562 100644 --- a/internal/outcome/sppulse_test.go +++ b/internal/outcome/sppulse_test.go @@ -143,12 +143,12 @@ func TestSpPulseOp(t *testing.T) { call("open", stub.ExpectArgs{"/proc/nonexistent/cookie"}, &stubOsFile{Reader: bytes.NewReader(sampleCookie)}, nil), }, newI(). // state.ensureRuntimeDir - Ensure(m(wantRuntimePath), 0700). + Ensure(m(wantRuntimePath), 0700, true). UpdatePermType(system.User, m(wantRuntimePath), acl.Execute). - Ensure(m(wantRunDirPath), 0700). + Ensure(m(wantRunDirPath), 0700, true). UpdatePermType(system.User, m(wantRunDirPath), acl.Execute). // state.runtime - Ephemeral(system.Process, m(wantRuntimeSharePath), 0700). + Ephemeral(system.Process, m(wantRuntimeSharePath), 0700, true). UpdatePerm(m(wantRuntimeSharePath), acl.Execute). // toSystem Link(m(wantRuntimePath+"/pulse/native"), m(wantRuntimeSharePath+"/pulse")), sysUsesRuntime(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{ @@ -175,12 +175,12 @@ func TestSpPulseOp(t *testing.T) { call("open", stub.ExpectArgs{"/proc/nonexistent/cookie"}, &stubOsFile{Reader: bytes.NewReader(sampleCookie[:len(sampleCookie)-0xe])}, nil), }, newI(). // state.ensureRuntimeDir - Ensure(m(wantRuntimePath), 0700). + Ensure(m(wantRuntimePath), 0700, true). UpdatePermType(system.User, m(wantRuntimePath), acl.Execute). - Ensure(m(wantRunDirPath), 0700). + Ensure(m(wantRunDirPath), 0700, true). UpdatePermType(system.User, m(wantRunDirPath), acl.Execute). // state.runtime - Ephemeral(system.Process, m(wantRuntimeSharePath), 0700). + Ephemeral(system.Process, m(wantRuntimeSharePath), 0700, true). UpdatePerm(m(wantRuntimeSharePath), acl.Execute). // toSystem Link(m(wantRuntimePath+"/pulse/native"), m(wantRuntimeSharePath+"/pulse")), sysUsesRuntime(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{ @@ -208,12 +208,12 @@ func TestSpPulseOp(t *testing.T) { call("open", stub.ExpectArgs{"/proc/nonexistent/cookie"}, &stubOsFile{Reader: bytes.NewReader(sampleCookie)}, nil), }, newI(). // state.ensureRuntimeDir - Ensure(m(wantRuntimePath), 0700). + Ensure(m(wantRuntimePath), 0700, true). UpdatePermType(system.User, m(wantRuntimePath), acl.Execute). - Ensure(m(wantRunDirPath), 0700). + Ensure(m(wantRunDirPath), 0700, true). UpdatePermType(system.User, m(wantRunDirPath), acl.Execute). // state.runtime - Ephemeral(system.Process, m(wantRuntimeSharePath), 0700). + Ephemeral(system.Process, m(wantRuntimeSharePath), 0700, true). UpdatePerm(m(wantRuntimeSharePath), acl.Execute). // toSystem Link(m(wantRuntimePath+"/pulse/native"), m(wantRuntimeSharePath+"/pulse")), sysUsesRuntime(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{ @@ -238,12 +238,12 @@ func TestSpPulseOp(t *testing.T) { call("verbose", stub.ExpectArgs{[]any{"cannot locate PulseAudio cookie (tried $PULSE_COOKIE, $XDG_CONFIG_HOME/pulse/cookie, $HOME/.pulse-cookie)"}}, nil, nil), }, newI(). // state.ensureRuntimeDir - Ensure(m(wantRuntimePath), 0700). + Ensure(m(wantRuntimePath), 0700, true). UpdatePermType(system.User, m(wantRuntimePath), acl.Execute). - Ensure(m(wantRunDirPath), 0700). + Ensure(m(wantRunDirPath), 0700, true). UpdatePermType(system.User, m(wantRunDirPath), acl.Execute). // state.runtime - Ephemeral(system.Process, m(wantRuntimeSharePath), 0700). + Ephemeral(system.Process, m(wantRuntimeSharePath), 0700, true). UpdatePerm(m(wantRuntimeSharePath), acl.Execute). // toSystem Link(m(wantRuntimePath+"/pulse/native"), m(wantRuntimeSharePath+"/pulse")), sysUsesRuntime(nil), nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{ diff --git a/internal/outcome/spruntime.go b/internal/outcome/spruntime.go index c08bfe81..b8121bfa 100644 --- a/internal/outcome/spruntime.go +++ b/internal/outcome/spruntime.go @@ -76,10 +76,9 @@ type spRuntimeOp struct { func (s *spRuntimeOp) toSystem(state *outcomeStateSys) error { if state.Container.Flags&hst.FShareRuntime != 0 { runtimeDir, runtimeDirInst := s.commonPaths(state.outcomeState) - state.sys.Ensure(runtimeDir, 0700) - state.sys.UpdatePermType(system.User, runtimeDir, acl.Execute) - state.sys.Ensure(runtimeDirInst, 0700) - state.sys.UpdatePermType(system.User, runtimeDirInst, acl.Read, acl.Write, acl.Execute) + state.sys.Ensure(runtimeDir, 0711, false) + state.sys.Ensure(runtimeDirInst, 0700, true) + state.sys.UpdatePermType(system.User, runtimeDirInst, acl.Read|acl.Write|acl.Execute) } if state.et&hst.EWayland != 0 { diff --git a/internal/outcome/spruntime_test.go b/internal/outcome/spruntime_test.go index 25f2684c..c549c857 100644 --- a/internal/outcome/spruntime_test.go +++ b/internal/outcome/spruntime_test.go @@ -33,10 +33,9 @@ func TestSpRuntimeOp(t *testing.T) { }, nil, []stub.Call{ // this op configures the system state and does not make calls during toSystem }, newI(). - Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0700). - UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime"), acl.Execute). - Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700). - UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(nil), []stub.Call{ + Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0711, false). + Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700, true). + UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read|acl.Write|acl.Execute), nil, nil, insertsOps(nil), []stub.Call{ // this op configures the container state and does not make calls during toContainer }, &container.Params{ Ops: new(container.Ops). @@ -61,10 +60,9 @@ func TestSpRuntimeOp(t *testing.T) { }, nil, []stub.Call{ // this op configures the system state and does not make calls during toSystem }, newI(). - Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0700). - UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime"), acl.Execute). - Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700). - UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(nil), []stub.Call{ + Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0711, false). + Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700, true). + UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read|acl.Write|acl.Execute), nil, nil, insertsOps(nil), []stub.Call{ // this op configures the container state and does not make calls during toContainer }, &container.Params{ Ops: new(container.Ops). @@ -89,10 +87,9 @@ func TestSpRuntimeOp(t *testing.T) { }, nil, []stub.Call{ // this op configures the system state and does not make calls during toSystem }, newI(). - Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0700). - UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime"), acl.Execute). - Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700). - UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(nil), []stub.Call{ + Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0711, false). + Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700, true). + UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read|acl.Write|acl.Execute), nil, nil, insertsOps(nil), []stub.Call{ // this op configures the container state and does not make calls during toContainer }, &container.Params{ Ops: new(container.Ops). @@ -113,10 +110,9 @@ func TestSpRuntimeOp(t *testing.T) { }, hst.Template, nil, []stub.Call{ // this op configures the system state and does not make calls during toSystem }, newI(). - Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0700). - UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime"), acl.Execute). - Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700). - UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(nil), []stub.Call{ + Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime"), 0711, false). + Ensure(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), 0700, true). + UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), acl.Read|acl.Write|acl.Execute), nil, nil, insertsOps(nil), []stub.Call{ // this op configures the container state and does not make calls during toContainer }, &container.Params{ Ops: new(container.Ops). diff --git a/internal/outcome/sptmpdir.go b/internal/outcome/sptmpdir.go index daa6f0de..c0c8db7b 100644 --- a/internal/outcome/sptmpdir.go +++ b/internal/outcome/sptmpdir.go @@ -19,10 +19,9 @@ type spTmpdirOp struct{} func (s spTmpdirOp) toSystem(state *outcomeStateSys) error { if state.Container.Flags&hst.FShareTmpdir != 0 { tmpdir, tmpdirInst := s.commonPaths(state.outcomeState) - state.sys.Ensure(tmpdir, 0700) - state.sys.UpdatePermType(system.User, tmpdir, acl.Execute) - state.sys.Ensure(tmpdirInst, 01700) - state.sys.UpdatePermType(system.User, tmpdirInst, acl.Read, acl.Write, acl.Execute) + state.sys.Ensure(tmpdir, 0711, false) + state.sys.Ensure(tmpdirInst, 01700, true) + state.sys.UpdatePermType(system.User, tmpdirInst, acl.Read|acl.Write|acl.Execute) } return nil } diff --git a/internal/outcome/sptmpdir_test.go b/internal/outcome/sptmpdir_test.go index 620f51b0..74aceeee 100644 --- a/internal/outcome/sptmpdir_test.go +++ b/internal/outcome/sptmpdir_test.go @@ -21,10 +21,9 @@ func TestSpTmpdirOp(t *testing.T) { }, hst.Template, nil, []stub.Call{ // this op configures the system state and does not make calls during toSystem }, newI(). - Ensure(m("/proc/nonexistent/tmp/hakurei.0/tmpdir"), 0700). - UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/tmpdir"), acl.Execute). - Ensure(m("/proc/nonexistent/tmp/hakurei.0/tmpdir/9"), 01700). - UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/tmpdir/9"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(nil), []stub.Call{ + Ensure(m("/proc/nonexistent/tmp/hakurei.0/tmpdir"), 0711, false). + Ensure(m("/proc/nonexistent/tmp/hakurei.0/tmpdir/9"), 01700, true). + UpdatePermType(system.User, m("/proc/nonexistent/tmp/hakurei.0/tmpdir/9"), acl.Read|acl.Write|acl.Execute), nil, nil, insertsOps(nil), []stub.Call{ // this op configures the container state and does not make calls during toContainer }, &container.Params{ Ops: new(container.Ops). diff --git a/internal/outcome/spwayland.go b/internal/outcome/spwayland.go index 3c60ff10..db79d1d2 100644 --- a/internal/outcome/spwayland.go +++ b/internal/outcome/spwayland.go @@ -49,7 +49,7 @@ func (s *spWaylandOp) toSystem(state *outcomeStateSys) error { state.msg.Verbose("direct wayland access, PROCEED WITH CAUTION") state.ensureRuntimeDir() s.SocketPath = socketPath - state.sys.UpdatePermType(hst.EWayland, socketPath, acl.Read, acl.Write, acl.Execute) + state.sys.UpdatePermType(hst.EWayland, socketPath, acl.Read|acl.Write|acl.Execute) } return nil } diff --git a/internal/outcome/spwayland_test.go b/internal/outcome/spwayland_test.go index 7cc4ba84..eea23136 100644 --- a/internal/outcome/spwayland_test.go +++ b/internal/outcome/spwayland_test.go @@ -34,7 +34,7 @@ func TestSpWaylandOp(t *testing.T) { call("lookupEnv", stub.ExpectArgs{"WAYLAND_DISPLAY"}, "wayland-1", nil), }, newI(). // state.instance - Ephemeral(system.Process, m(wantInstancePrefix), 0711). + Ephemeral(system.Process, m(wantInstancePrefix), 0711, false). // toSystem Wayland( m(wantInstancePrefix+"/wayland"), @@ -64,12 +64,12 @@ func TestSpWaylandOp(t *testing.T) { call("verbose", stub.ExpectArgs{[]any{"direct wayland access, PROCEED WITH CAUTION"}}, nil, nil), }, newI(). // state.ensureRuntimeDir - Ensure(m(wantRuntimePath), 0700). + Ensure(m(wantRuntimePath), 0700, true). UpdatePermType(system.User, m(wantRuntimePath), acl.Execute). - Ensure(m(wantRunDirPath), 0700). + Ensure(m(wantRunDirPath), 0700, true). UpdatePermType(system.User, m(wantRunDirPath), acl.Execute). // toSystem - UpdatePermType(hst.EWayland, m("/proc/nonexistent/wayland"), acl.Read, acl.Write, acl.Execute), nil, nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{ + UpdatePermType(hst.EWayland, m("/proc/nonexistent/wayland"), acl.Read|acl.Write|acl.Execute), nil, nil, insertsOps(afterSpRuntimeOp(nil)), []stub.Call{ // this op configures the container state and does not make calls during toContainer }, &container.Params{ Ops: new(container.Ops). @@ -85,7 +85,7 @@ func TestSpWaylandOp(t *testing.T) { call("verbose", stub.ExpectArgs{[]any{"WAYLAND_DISPLAY is not set, assuming wayland-0"}}, nil, nil), }, newI(). // state.instance - Ephemeral(system.Process, m(wantInstancePrefix), 0711). + Ephemeral(system.Process, m(wantInstancePrefix), 0711, false). // toSystem Wayland( m(wantInstancePrefix+"/wayland"), diff --git a/internal/outcome/spx11.go b/internal/outcome/spx11.go index c71ff530..ebd5c77e 100644 --- a/internal/outcome/spx11.go +++ b/internal/outcome/spx11.go @@ -56,7 +56,7 @@ func (s *spX11Op) toSystem(state *outcomeStateSys) error { ), Err: err} } } else { - state.sys.UpdatePermType(hst.EX11, socketPath, acl.Read, acl.Write, acl.Execute) + state.sys.UpdatePermType(hst.EX11, socketPath, acl.Read|acl.Write|acl.Execute) if state.Container.Flags&hst.FHostAbstract == 0 { s.Display = "unix:" + socketPath.String() } diff --git a/internal/outcome/spx11_test.go b/internal/outcome/spx11_test.go index 0ede2d65..d183f798 100644 --- a/internal/outcome/spx11_test.go +++ b/internal/outcome/spx11_test.go @@ -83,7 +83,7 @@ func TestSpX11Op(t *testing.T) { call("lookupEnv", stub.ExpectArgs{"DISPLAY"}, "unix:/tmp/.X11-unix/X0", nil), call("stat", stub.ExpectArgs{"/tmp/.X11-unix/X0"}, (*stubFi)(nil), nil), }, newI(). - UpdatePermType(hst.EX11, m("/tmp/.X11-unix/X0"), acl.Read, acl.Write, acl.Execute). + UpdatePermType(hst.EX11, m("/tmp/.X11-unix/X0"), acl.Read|acl.Write|acl.Execute). ChangeHosts("#10009"), nil, nil, insertsOps(nil), []stub.Call{ // this op configures the container state and does not make calls during toContainer }, &container.Params{ @@ -106,7 +106,7 @@ func TestSpX11Op(t *testing.T) { call("lookupEnv", stub.ExpectArgs{"DISPLAY"}, ":0", nil), call("stat", stub.ExpectArgs{"/tmp/.X11-unix/X0"}, (*stubFi)(nil), nil), }, newI(). - UpdatePermType(hst.EX11, m("/tmp/.X11-unix/X0"), acl.Read, acl.Write, acl.Execute). + UpdatePermType(hst.EX11, m("/tmp/.X11-unix/X0"), acl.Read|acl.Write|acl.Execute). ChangeHosts("#10009"), nil, nil, insertsOps(nil), []stub.Call{ // this op configures the container state and does not make calls during toContainer }, &container.Params{ diff --git a/internal/rosa/llvm.go b/internal/rosa/llvm.go index 6d1dcf0b..08625fcf 100644 --- a/internal/rosa/llvm.go +++ b/internal/rosa/llvm.go @@ -145,6 +145,8 @@ func init() { "global-value-regex", "global-hex-value-regex", "check-globals", + // clang: expected string not found in input + "Interpreter/failed-input-keeps-redecls.cpp", // cxx: fails on musl "close.dont-get-rid-of-buffer", "re/re.traits", diff --git a/internal/rosa/llvm_test.go b/internal/rosa/llvm_test.go index 3152107c..3fa8fc9d 100644 --- a/internal/rosa/llvm_test.go +++ b/internal/rosa/llvm_test.go @@ -8,7 +8,7 @@ import ( ) func TestLLVMInputs(t *testing.T) { - const wantInputCount = 466 + const wantInputCount = 472 _, llvm := rosa.MustLoad(rosa.H("llvm")) var n int diff --git a/internal/rosa/mirror.go b/internal/rosa/mirror.go index 4af931c7..20dd3231 100644 --- a/internal/rosa/mirror.go +++ b/internal/rosa/mirror.go @@ -451,7 +451,6 @@ func writeF[B any]( return } m.msg.GetLogger().Println(err) - w.WriteHeader(http.StatusInternalServerError) } // Register configures an [http.ServeMux] for servicing mirror requests. diff --git a/internal/rosa/package/acl/package.az b/internal/rosa/package/acl/package.az index 0e06f227..06c9abe2 100644 --- a/internal/rosa/package/acl/package.az +++ b/internal/rosa/package/acl/package.az @@ -5,7 +5,7 @@ package attr { version# = "2.6.0"; source = remoteTar { - url = "https://download.savannah.nongnu.org/releases/attr/"+ + url = "https://download-mirror.savannah.nongnu.org/releases/attr/"+ "attr-"+version+".tar.gz"; checksum = "pp-NvD1cMIwZycNwZGW2ez-PbTEpHxrRnVH27csj9QdN4oEBkEbJOZX1IIvKnTWS"; compress = gzip; @@ -28,7 +28,7 @@ package acl { version# = "2.4.0"; source = remoteTar { - url = "https://download.savannah.nongnu.org/releases/acl/"+ + url = "https://download-mirror.savannah.nongnu.org/releases/acl/"+ "acl-"+version+".tar.gz"; checksum = "U2eaAsWrhhzEfhyprwHQQG55bFPZxtsHk1Usnd9Jb_g-H__pJod4H3RTrL4ipXDw"; compress = gzip; diff --git a/internal/rosa/package/cairo.az b/internal/rosa/package/cairo.az index 7ffbb304..abe0122c 100644 --- a/internal/rosa/package/cairo.az +++ b/internal/rosa/package/cairo.az @@ -3,12 +3,12 @@ package cairo { website = "https://www.cairographics.org"; anitya = 247; - version# = "1.18.4"; + version# = "1.18.6"; source = remoteGitLab { domain = "gitlab.freedesktop.org"; suffix = "cairo/cairo"; ref = version; - checksum = "_Ztw0ttmYLIiElqS27x2Iu4EneXOMNHp2or6bJA6suLeF3NRvORzUMPPQy9Wr5gT"; + checksum = "xkXsQ_D8ZH5YF6tldJLRazKjP04xXu3RgDiTEbCwKP6s5ot_5VDfljj2gwnFOoc6"; }; exec = meson { diff --git a/internal/rosa/package/cmake/package.az b/internal/rosa/package/cmake/package.az index 1e6ea8b3..bd67abb8 100644 --- a/internal/rosa/package/cmake/package.az +++ b/internal/rosa/package/cmake/package.az @@ -3,12 +3,12 @@ package cmake { website = "https://cmake.org"; anitya = 306; - version# = "4.4.3"; + version# = "4.4.4"; source = remoteGitHubRelease { suffix = "Kitware/CMake"; tag = "v"+version; name = "cmake-"+version+".tar.gz"; - checksum = "69yrNbgUPZXY0phavZEMPRgBu_6858LHzLoyHk1aeTuXf8RW5Ec37lr2OBaGZ5PY"; + checksum = "I7vJROVzty0xtoWvDQzR5JpnEl-gO-1kXd-qIO2lWgj65Hgyd4kA08mM1wk5l8z3"; compress = gzip; }; patches = [ "disable-broken-tests-musl.patch" ]; diff --git a/internal/rosa/package/cryptsetup.az b/internal/rosa/package/cryptsetup.az index 0886d222..2c3b679c 100644 --- a/internal/rosa/package/cryptsetup.az +++ b/internal/rosa/package/cryptsetup.az @@ -3,12 +3,12 @@ package cryptsetup { website = "https://gitlab.com/cryptsetup/cryptsetup"; anitya = 13709; - version# = "2.8.7"; + version# = "2.8.8"; source = remoteGitLab { domain = "gitlab.com"; suffix = "cryptsetup/cryptsetup"; ref = "v"+version; - checksum = "eDi_UaMtwhIzqpsdcA91aVb56zCYDrsDzeMWfWXAnPbuvn2bEgeH5dFs7mouHz8V"; + checksum = "Q88h-7N-QFJcBe-MbtkFu0fSjrhRUF8WYCH4vhznbtFjniXLXRKMku-JoZAtHQMS"; }; bin = [ "bash" ]; diff --git a/internal/rosa/package/curl.az b/internal/rosa/package/curl.az index a54deccc..0f10763d 100644 --- a/internal/rosa/package/curl.az +++ b/internal/rosa/package/curl.az @@ -3,10 +3,10 @@ package curl { website = "https://curl.se"; anitya = 381; - version# = "8.22.0"; + version# = "8.23.0"; source = remoteTar { url = "https://curl.se/download/curl-"+version+".tar.bz2"; - checksum = "zeIqlmHElDa3IPLZq86ICRxn1YoiXlNCdx30BuahcUToYqjSuyvu8AtRqbzKH_06"; + checksum = "FBVGKlIt-kkyHMcu7SsNosovyg3jNiaQ4GrHEFzApJ_OviyYPaUcacOpsHYUGV40"; compress = bzip2; }; diff --git a/internal/rosa/package/dbus.az b/internal/rosa/package/dbus.az index 4d2dd6ec..402cc6fb 100644 --- a/internal/rosa/package/dbus.az +++ b/internal/rosa/package/dbus.az @@ -36,11 +36,11 @@ package xdg-dbus-proxy { website = "https://github.com/flatpak/xdg-dbus-proxy"; anitya = 58434; - version# = "0.1.8"; + version# = "0.1.9"; source = remoteGitHub { suffix = "flatpak/xdg-dbus-proxy"; tag = version; - checksum = "ygdW0EEvZ-_PJfnFTNoobITmRzqJecTinBsJd_iJQrRvzidwqESCjeNkB55smZ2y"; + checksum = "HfAi2M60v3wE_TbESYZ4eMB4G2-spBt0x0UrKaRp7FNVywDbg9d_L3_kr2QXzlfp"; }; exec = meson { diff --git a/internal/rosa/package/firmware.az b/internal/rosa/package/firmware.az index f04b39e9..e87a34c1 100644 --- a/internal/rosa/package/firmware.az +++ b/internal/rosa/package/firmware.az @@ -3,12 +3,12 @@ package firmware { website = "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git"; anitya = 141464; - version# = "20260810"; + version# = "20260916"; source = remoteGitLab { domain = "gitlab.com"; suffix = "kernel-firmware/linux-firmware"; ref = version; - checksum = "XNUhz9QstaDKNegM3Bl0-TtZzpsvX7-E8WMnDCO7KBhBC6t_TxsdG05-ETdXynry"; + checksum = "Yv3yaffN6-ARSFSnDm-0gHM3T7m3ZwMqBLIP8UA3Jpe23Jk8MyNLUE44wJMy_DH6"; }; // dedup creates temporary file diff --git a/internal/rosa/package/freetype.az b/internal/rosa/package/freetype.az index cfa5933a..6f240bf3 100644 --- a/internal/rosa/package/freetype.az +++ b/internal/rosa/package/freetype.az @@ -3,11 +3,11 @@ package freetype { website = "http://www.freetype.org"; anitya = 854; - version# = "2.14.3"; + version# = "2.15.0"; source = remoteTar { - url = "https://download.savannah.gnu.org/releases/freetype/"+ + url = "https://download-mirror.savannah.gnu.org/releases/freetype/"+ "freetype-"+version+".tar.gz"; - checksum = "-WfLv8fVJNyCHpP_lriiDzOcVbBL9ajdQ3tl8AzIIUa9-8sVpU9irxOmSMgRHWYz"; + checksum = "zjiWtj5ry7tnc5libuhSv3Ik3F8rAoK0j7abCmdTxulPdKYEjAsbLXAGkTq-IOJ4"; compress = gzip; }; diff --git a/internal/rosa/package/fribidi.az b/internal/rosa/package/fribidi.az index 0bd45693..28df4375 100644 --- a/internal/rosa/package/fribidi.az +++ b/internal/rosa/package/fribidi.az @@ -3,11 +3,11 @@ package fribidi { website = "https://github.com/fribidi/fribidi"; anitya = 857; - version# = "1.0.16"; + version# = "1.0.17"; source = remoteGitHub { suffix = "fribidi/fribidi"; tag = "v"+version; - checksum = "Sn9hwdgViXW9_MOi8ovAcBgQy6s6o4cc0o260k76nqCRzxmxxD1-B37GVkJ2EoMy"; + checksum = "yJWcJqYDmzdyFB-kc9J1JDPLiqWHTw2n2bheXvqzP66bk56HYWW9sz_w-TMUqiEV"; }; exec = meson { diff --git a/internal/rosa/package/fuse/package.az b/internal/rosa/package/fuse/package.az index 31000e07..3e43c3c6 100644 --- a/internal/rosa/package/fuse/package.az +++ b/internal/rosa/package/fuse/package.az @@ -3,12 +3,12 @@ package fuse { website = "https://github.com/libfuse/libfuse"; anitya = 861; - version# = "3.18.2"; + version# = "3.18.3"; source = remoteGitHubRelease { suffix = "libfuse/libfuse"; tag = "fuse-"+version; name = "fuse-"+version+".tar.gz"; - checksum = "iL-7b7eUtmlVSf5cSq0dzow3UiqSjBmzV3cI_ENPs1tXcHdktkG45j1V12h-4jZe"; + checksum = "zQuo1ZLCePjlgxbOO8BuiLPPHwjbU0IItSo3K9_BtcxIid027b7XQWfm07c1T5D4"; compress = gzip; }; patches = [ "example-include.patch" ]; @@ -20,12 +20,8 @@ package fuse { "useroot": "false"; "initscriptdir": "/system/etc"; }; - - postCompile = "python3 -m pytest test/"; - // this project uses pytest - test = false; }; - tools = [ python-pytest ]; + tools = [ bash ]; inputs = [ kernel-headers ]; } diff --git a/internal/rosa/package/git.az b/internal/rosa/package/git.az index 07abe88e..42d4665d 100644 --- a/internal/rosa/package/git.az +++ b/internal/rosa/package/git.az @@ -3,11 +3,11 @@ package git { website = "https://www.git-scm.com"; anitya = 5350; - version# = "2.55.0"; + version# = "2.56.0"; source = remoteTar { url = "https://www.kernel.org/pub/software/scm/git/"+ "git-"+version+".tar.gz"; - checksum = "lOer6jb8vZQk6Nd1rLZIgsYys8whLvjJq8XbmCdTATcPFAYuQcK1Rgj1Jj6a00W8"; + checksum = "9HMTVgQnHmUH9v8ot_DXl5mI8tGMcbilKLUzgFXg1k8Z5YbzxkF6b-ngTL4ZgeYN"; compress = gzip; }; @@ -63,6 +63,8 @@ disable_test t5515-fetch-merge-logic disable_test t4124-apply-ws-rule disable_test t3404-rebase-interactive disable_test t7527-builtin-fsmonitor +disable_test t5702-protocol-v2 +disable_test t3600-rm `; check = [ diff --git a/internal/rosa/package/glib/loopvar-cast.patch b/internal/rosa/package/glib/loopvar-cast.patch new file mode 100644 index 00000000..fd4f8c45 --- /dev/null +++ b/internal/rosa/package/glib/loopvar-cast.patch @@ -0,0 +1,13 @@ +diff --git a/glib/tests/string.c b/glib/tests/string.c +index 3d9229616..9553c9217 100644 +--- a/glib/tests/string.c ++++ b/glib/tests/string.c +@@ -348,7 +348,7 @@ test_string_prepend_c (void) + string = g_string_new ("hi pete!"); + + for (i = 0; i < 10000; i++) +- g_string_prepend_c (string, 'a'+(i%26)); ++ g_string_prepend_c (string, 'a'+(gchar)(i%26)); + + g_assert((strlen("hi pete!") + 10000) == string->len); + g_assert((strlen("hi pete!") + 10000) == strlen(string->str)); diff --git a/internal/rosa/package/glib/package.az b/internal/rosa/package/glib/package.az index 14ff0450..7f3040e0 100644 --- a/internal/rosa/package/glib/package.az +++ b/internal/rosa/package/glib/package.az @@ -3,12 +3,13 @@ package glib { website = "https://developer.gnome.org/glib"; anitya = 10024; - version# = "2.89.4"; + version# = "2.90.1"; source = remoteGit { url = "https://gitlab.gnome.org/GNOME/glib.git"; tag = version; - checksum = "I_nTEVp4rpOQ4VCmJpesjvq3VbFJ3oLRAUVvsdJEfFiY4XXsvi0ITTqCEWe9Lr47"; + checksum = "h3Chw60ZesAtUzKQfHKsMvsfkEAK0HaTSV8ZANJK7vkTtb9f7D1P763_vROClV3Z"; }; + patches = [ "loopvar-cast.patch" ]; files = { "/etc/machine-id": "stub-machine-id-fhs"; diff --git a/internal/rosa/package/glslang.az b/internal/rosa/package/glslang.az index 3cbd4fa0..1aca89c6 100644 --- a/internal/rosa/package/glslang.az +++ b/internal/rosa/package/glslang.az @@ -3,11 +3,11 @@ package glslang { website = "https://github.com/KhronosGroup/glslang"; anitya = 334917; - version# = "1.4.357.0"; + version# = "1.4.363.0"; source = remoteGitHub { suffix = "KhronosGroup/glslang"; tag = "vulkan-sdk-"+version; - checksum = "7Zq12xtSd5z_aEEeYCnsWR5WvcEGVlTsRaV6MqkmY1a3iNIrRbX_rrg-taI3wMe6"; + checksum = "ATWNapAERPA42c5EKZNnYj37M54wkaOCx5L-QWlZe9EfKVomP-s4FHieiJYg4FQI"; }; // test suite writes to source @@ -18,6 +18,8 @@ package glslang { cache = { "BUILD_SHARED_LIBS": "ON"; "ALLOW_EXTERNAL_SPIRV_TOOLS": "ON"; + + "CMAKE_EXE_LINKER_FLAGS": "-Wl,-z,stack-size=1048576"; }; }; diff --git a/internal/rosa/package/gnu.az b/internal/rosa/package/gnu.az index 3eb48ab5..e117173e 100644 --- a/internal/rosa/package/gnu.az +++ b/internal/rosa/package/gnu.az @@ -66,10 +66,10 @@ package automake { website = "https://www.gnu.org/software/automake"; anitya = 144; - version# = "1.18.1"; + version# = "1.19"; source = remoteTar { url = "https://mirrors.kernel.org/gnu/automake/automake-"+version+".tar.gz"; - checksum = "FjvLG_GdQP7cThTZJLDMxYpRcKdpAVG-YDs1Fj1yaHlSdh_Kx6nRGN14E0r_BjcG"; + checksum = "p-EeV6uh3lSbLtsnxI-l0HxC_QAnANoEdrQTI2l_VCRtYQpvmbwXIglo8S6DAI7G"; compress = gzip; }; @@ -125,24 +125,6 @@ package libtool { ]; } -package gzip { - description = "a popular data compression program"; - website = "https://www.gnu.org/software/gzip"; - anitya = 1290; - - version# = "1.14"; - source = remoteTar { - url = "https://mirrors.kernel.org/gnu/gzip/gzip-"+version+".tar.gz"; - checksum = "NWhjUavnNfTDFkZJyAUonL9aCOak8GVajWX2OMlzpFnuI0ErpBFyj88mz2xSjz0q"; - compress = gzip; - }; - - exec = make { - // dependency loop - check = nil; - }; -} - package sed { description = "a non-interactive command-line text editor"; website = "https://www.gnu.org/software/sed"; @@ -339,10 +321,10 @@ package coreutils { website = "https://www.gnu.org/software/coreutils"; anitya = 343; - version# = "9.11"; + version# = "9.12"; source = remoteTar { url = "https://mirrors.kernel.org/gnu/coreutils/coreutils-"+version+".tar.gz"; - checksum = "t8UMed5wpFEoC56aa42_yidfOAaRGzOfj7MRtQkkqgGbpXiskNA8bd-EmVSQkZie"; + checksum = "MYYday_Pwp2YFSgVHeCQWJBhJCBhqkIkfML4_a_uHvM_FsKbYQ4vbXy60FprcUoo"; compress = gzip; }; @@ -497,10 +479,10 @@ package parallel { website = "https://www.gnu.org/software/parallel"; anitya = 5448; - version# = "20260822"; + version# = "20260922"; source = remoteTar { url = "https://mirrors.kernel.org/gnu/parallel/parallel-"+version+".tar.bz2"; - checksum = "zf-5ZEpAZ073coMSyOY7NRmXwCRPWvpjbjOMOvirU9fHlU0WWovwss-bAMrh0q_G"; + checksum = "4M2gBy-esCSvR9ub6SUnoVHz7p3ZDhdeJXm7yv2C4iJQdXyVNkYqgusPFsvH8MCp"; compress = bzip2; }; @@ -520,10 +502,10 @@ package libunistring { website = "https://www.gnu.org/software/libunistring"; anitya = 1747; - version# = "1.4.2"; + version# = "1.5"; source = remoteTar { url = "https://mirrors.kernel.org/gnu/libunistring/libunistring-"+version+".tar.gz"; - checksum = "iW9BbfLoVlXjWoLTZ4AekQSu4cFBnLcZ4W8OHWbv0AhJNgD3j65_zqaLMzFKylg2"; + checksum = "6r3gdRT3T5j1YPjoRBnRMY6uXVJuRdzu8eir1qeHcErEJegjlbMux2IDP6XPJyDX"; compress = gzip; }; diff --git a/internal/rosa/package/go.az b/internal/rosa/package/go.az index 89d3732d..08ad847e 100644 --- a/internal/rosa/package/go.az +++ b/internal/rosa/package/go.az @@ -3,10 +3,10 @@ package go { website = "https://go.dev"; anitya = 1227; - version# = "1.27.1"; + version# = "1.27.2"; source = remoteTar { url = "https://go.dev/dl/go"+version+".src.tar.gz"; - checksum = "Jw7OCTZbixLPF0E2WafFBh6DnxbsElvEg_RsMGkrzAmi0pyJTI3eQqLJsvXlVtbK"; + checksum = "Vxj6mhsDZEPq3AX21AA20zWMaVPX3Ezhdbkm1SK8O__BH8jczdeeLFMasQ-PUIww"; compress = gzip; }; diff --git a/internal/rosa/package/gzip/late-include.patch b/internal/rosa/package/gzip/late-include.patch new file mode 100644 index 00000000..4638d578 --- /dev/null +++ b/internal/rosa/package/gzip/late-include.patch @@ -0,0 +1,21 @@ +diff --git a/gzip.c b/gzip.c +index 220f6fc..ef79a89 100644 +--- a/gzip.c ++++ b/gzip.c +@@ -56,6 +56,8 @@ static char const license_msg[] = + + #include <config.h> + ++#include <signal.h> ++ + #include "tailor.h" + + #include "gzip.h" +@@ -79,7 +81,6 @@ static char const license_msg[] = + #include <inttypes.h> + #include <limits.h> + #include <locale.h> +-#include <signal.h> + #include <stdcountof.h> + #include <stddef.h> + #include <stdlib.h> diff --git a/internal/rosa/package/gzip/package.az b/internal/rosa/package/gzip/package.az new file mode 100644 index 00000000..6c6b3b1f --- /dev/null +++ b/internal/rosa/package/gzip/package.az @@ -0,0 +1,18 @@ +package gzip { + description = "a popular data compression program"; + website = "https://www.gnu.org/software/gzip"; + anitya = 1290; + + version# = "1.15"; + source = remoteTar { + url = "https://mirrors.kernel.org/gnu/gzip/gzip-"+version+".tar.gz"; + checksum = "N8v7cH-Vhmxvk1PYaF-B1vH-r-rMT3Z3VyMdCFXhBd74a8v62wT3dKes-u1JCvUG"; + compress = gzip; + }; + patches = [ "late-include.patch" ]; + + exec = make { + // dependency loop + check = nil; + }; +} diff --git a/internal/rosa/package/hakurei.az b/internal/rosa/package/hakurei.az index 4e9a8d2c..5585002c 100644 --- a/internal/rosa/package/hakurei.az +++ b/internal/rosa/package/hakurei.az @@ -4,7 +4,7 @@ package hakurei-source { version# = "0.4.8"; output = remoteGit { - url = "https://git.gensokyo.uk/rosa/hakurei.git"; + url = "https://src.rosa.moe/hakurei"; tag = "v"+version; checksum = "B6aoojyXblZ7eYwdc3Ak3wDbBpu63j8S79Gpiul9Obb8diqzBIJ2qNCgWVH6qLsc"; }; @@ -75,6 +75,7 @@ mkdir -p /work/system/bin/ xcb, wayland, wayland-protocols, + util-linux, kernel-headers, ]; @@ -121,6 +122,55 @@ DESTDIR=/work "./${NAME}.sh" xcb, wayland, wayland-protocols, + util-linux, + kernel-headers, + ]; + + runtime = nil; +} + +package hakurei-dist-race { + description = "low-level userspace tooling for Rosa OS (distribution tarball with race instrument)"; + website = "https://hakurei.app"; + + source = hakurei-source; + enterSource = true; + writable = true; + chmod = true; + + env = [ + "CGO_ENABLED=1", + "GOCACHE=/tmp/gocache", + "CC=clang -O3 -Werror", + "HAKUREI_RACE=1", + ]; + + exec = generic { + chdir = false; + build = ` +NAME=make`; + check = ` +NAME=all`; + install = ` +DESTDIR=/work "./${NAME}.sh" +`; + }; + + tools = [ + go, + pkgconf, + net-tools, + acl, + ]; + + inputs = [ + libseccomp, + acl, + fuse, + xcb, + wayland, + wayland-protocols, + util-linux, kernel-headers, ]; diff --git a/internal/rosa/package/harfbuzz.az b/internal/rosa/package/harfbuzz.az index 7e10f0f2..2d62d023 100644 --- a/internal/rosa/package/harfbuzz.az +++ b/internal/rosa/package/harfbuzz.az @@ -3,11 +3,11 @@ package harfbuzz { website = "https://harfbuzz.github.io"; anitya = 1299; - version# = "14.4.0"; + version# = "14.6.0"; source = remoteGitHub { suffix = "harfbuzz/harfbuzz"; tag = version; - checksum = "Avi4h70Src2_E7l8ag1NP64KH2moVvAWb_cCay4b9FB_5zAJzcTTJLCTf_-nnCHw"; + checksum = "raMMlGfzwy1bEIIO8a93-eEj50ksDDWDdLyeQlO8IM04_woqkIPhJ90QvU3lZJ2L"; }; exec = meson { diff --git a/internal/rosa/package/hwdata.az b/internal/rosa/package/hwdata.az index 57bde7e8..5f853a0a 100644 --- a/internal/rosa/package/hwdata.az +++ b/internal/rosa/package/hwdata.az @@ -3,11 +3,11 @@ package hwdata { website = "https://github.com/vcrhonek/hwdata"; anitya = 5387; - version# = "0.411"; + version# = "0.412"; source = remoteGitHub { suffix = "vcrhonek/hwdata"; tag = "v"+version; - checksum = "5YWsZC8NgZShWWHmC66xqEkTJLAUYJoDt8kmstJyLWfsNqAWAhJZGkVUfgqbTv-6"; + checksum = "_HkJ6nni1s_D_CEPUM905AaQkE8v-CorQOv9sB8x0EafeTeOV86b8l-SqL6i7DJL"; }; writable = true; diff --git a/internal/rosa/package/kernel/amd64.config b/internal/rosa/package/kernel/amd64.config index b52bfbd4..cd8f1132 100644 --- a/internal/rosa/package/kernel/amd64.config +++ b/internal/rosa/package/kernel/amd64.config @@ -1,16 +1,16 @@ # # Automatically generated file; DO NOT EDIT. -# Linux/x86 6.18.49 Kernel Configuration +# Linux/x86 6.18.57 Kernel Configuration # -CONFIG_CC_VERSION_TEXT="clang version 23.1.0" +CONFIG_CC_VERSION_TEXT="clang version 23.1.3" CONFIG_GCC_VERSION=0 CONFIG_CC_IS_CLANG=y -CONFIG_CLANG_VERSION=230100 +CONFIG_CLANG_VERSION=230103 CONFIG_AS_IS_LLVM=y -CONFIG_AS_VERSION=230100 +CONFIG_AS_VERSION=230103 CONFIG_LD_VERSION=0 CONFIG_LD_IS_LLD=y -CONFIG_LLD_VERSION=230100 +CONFIG_LLD_VERSION=230103 CONFIG_RUSTC_VERSION=0 CONFIG_RUSTC_LLVM_VERSION=0 CONFIG_CC_HAS_ASM_GOTO_OUTPUT=y @@ -21,6 +21,7 @@ CONFIG_CC_HAS_ASSUME=y CONFIG_CC_HAS_NO_PROFILE_FN_ATTR=y CONFIG_CC_HAS_COUNTED_BY=y CONFIG_CC_HAS_MULTIDIMENSIONAL_NONSTRING=y +CONFIG_CC_OPT_INLINE_MEMSET="-mllvm -max-store-memset=4294967294" CONFIG_LD_CAN_USE_KEEP_IN_OVERLAY=y CONFIG_PAHOLE_VERSION=0 CONFIG_IRQ_WORK=y @@ -658,7 +659,7 @@ CONFIG_ACPI_IPMI=m CONFIG_ACPI_HOTPLUG_CPU=y CONFIG_ACPI_PROCESSOR_AGGREGATOR=m CONFIG_ACPI_THERMAL=m -CONFIG_ACPI_PLATFORM_PROFILE=m +CONFIG_ACPI_PLATFORM_PROFILE=y CONFIG_ACPI_CUSTOM_DSDT_FILE="" CONFIG_ARCH_HAS_ACPI_TABLE_UPGRADE=y CONFIG_ACPI_TABLE_UPGRADE=y @@ -730,6 +731,7 @@ CONFIG_X86_INTEL_PSTATE=y CONFIG_X86_PCC_CPUFREQ=m CONFIG_X86_AMD_PSTATE=y CONFIG_X86_AMD_PSTATE_DEFAULT_MODE=3 +CONFIG_X86_AMD_PSTATE_DYNAMIC_EPP=y CONFIG_X86_AMD_PSTATE_UT=m CONFIG_X86_ACPI_CPUFREQ=m CONFIG_X86_ACPI_CPUFREQ_CPB=y @@ -1044,6 +1046,7 @@ CONFIG_FUNCTION_ALIGNMENT_16B=y CONFIG_FUNCTION_ALIGNMENT=16 CONFIG_CC_HAS_SANE_FUNCTION_ALIGNMENT=y CONFIG_ARCH_HAS_CPU_ATTACK_VECTORS=y +CONFIG_HAVE_ARCH_GET_SECUREBOOT=y # end of General architecture-dependent options CONFIG_RT_MUTEXES=y diff --git a/internal/rosa/package/kernel/arm64.config b/internal/rosa/package/kernel/arm64.config index 9c6951b7..8af9d893 100644 --- a/internal/rosa/package/kernel/arm64.config +++ b/internal/rosa/package/kernel/arm64.config @@ -1,16 +1,16 @@ # # Automatically generated file; DO NOT EDIT. -# Linux/arm64 6.18.49 Kernel Configuration +# Linux/arm64 6.18.57 Kernel Configuration # -CONFIG_CC_VERSION_TEXT="clang version 23.1.0" +CONFIG_CC_VERSION_TEXT="clang version 23.1.3" CONFIG_GCC_VERSION=0 CONFIG_CC_IS_CLANG=y -CONFIG_CLANG_VERSION=230100 +CONFIG_CLANG_VERSION=230103 CONFIG_AS_IS_LLVM=y -CONFIG_AS_VERSION=230100 +CONFIG_AS_VERSION=230103 CONFIG_LD_VERSION=0 CONFIG_LD_IS_LLD=y -CONFIG_LLD_VERSION=230100 +CONFIG_LLD_VERSION=230103 CONFIG_RUSTC_VERSION=0 CONFIG_RUSTC_LLVM_VERSION=0 CONFIG_CC_HAS_ASM_GOTO_OUTPUT=y @@ -21,6 +21,7 @@ CONFIG_CC_HAS_ASSUME=y CONFIG_CC_HAS_NO_PROFILE_FN_ATTR=y CONFIG_CC_HAS_COUNTED_BY=y CONFIG_CC_HAS_MULTIDIMENSIONAL_NONSTRING=y +CONFIG_CC_OPT_INLINE_MEMSET="-mllvm -max-store-memset=4294967294" CONFIG_LD_CAN_USE_KEEP_IN_OVERLAY=y CONFIG_PAHOLE_VERSION=0 CONFIG_IRQ_WORK=y @@ -456,8 +457,10 @@ CONFIG_ARM64_ERRATUM_2224489=y CONFIG_ARM64_ERRATUM_2064142=y CONFIG_ARM64_ERRATUM_2038923=y CONFIG_ARM64_ERRATUM_1902691=y +CONFIG_ARM64_WORKAROUND_BROKEN_AMU_CONSTCNT=y CONFIG_ARM64_ERRATUM_2457168=y CONFIG_ARM64_ERRATUM_2645198=y +CONFIG_ARM64_ERRATUM_3821522=y CONFIG_ARM64_WORKAROUND_SPECULATIVE_UNPRIV_LOAD=y CONFIG_ARM64_ERRATUM_2966298=y CONFIG_ARM64_ERRATUM_3117295=y @@ -575,7 +578,6 @@ CONFIG_ARM64_TLB_RANGE=y # CONFIG_AS_HAS_ARMV8_5=y CONFIG_ARM64_BTI=y -CONFIG_ARM64_BTI_KERNEL=y CONFIG_CC_HAS_BRANCH_PROT_PAC_RET_BTI=y CONFIG_ARM64_E0PD=y CONFIG_ARM64_AS_HAS_MTE=y @@ -975,6 +977,7 @@ CONFIG_HAVE_GCC_PLUGINS=y CONFIG_FUNCTION_ALIGNMENT_4B=y CONFIG_FUNCTION_ALIGNMENT=4 CONFIG_CC_HAS_SANE_FUNCTION_ALIGNMENT=y +CONFIG_HAVE_ARCH_GET_SECUREBOOT=y # end of General architecture-dependent options CONFIG_RT_MUTEXES=y @@ -12217,9 +12220,10 @@ CONFIG_PXA1908_PM_DOMAINS=m # CONFIG_MTK_SCPSYS=y CONFIG_MTK_SCPSYS_PM_DOMAINS=y -CONFIG_AIROHA_CPU_PM_DOMAIN=m # end of MediaTek PM Domains +CONFIG_AIROHA_CPU_PM_DOMAIN=m + # # Qualcomm PM Domains # @@ -14220,11 +14224,9 @@ CONFIG_CRYPTO_DEV_SUN4I_SS=m CONFIG_CRYPTO_DEV_SUN8I_CE=m # CONFIG_CRYPTO_DEV_SUN8I_CE_DEBUG is not set # CONFIG_CRYPTO_DEV_SUN8I_CE_HASH is not set -# CONFIG_CRYPTO_DEV_SUN8I_CE_PRNG is not set # CONFIG_CRYPTO_DEV_SUN8I_CE_TRNG is not set CONFIG_CRYPTO_DEV_SUN8I_SS=m # CONFIG_CRYPTO_DEV_SUN8I_SS_DEBUG is not set -# CONFIG_CRYPTO_DEV_SUN8I_SS_PRNG is not set # CONFIG_CRYPTO_DEV_SUN8I_SS_HASH is not set CONFIG_CRYPTO_DEV_FSL_CAAM_COMMON=m CONFIG_CRYPTO_DEV_FSL_CAAM_CRYPTO_API_DESC=m diff --git a/internal/rosa/package/kernel/dml-frame_warn_limit.patch b/internal/rosa/package/kernel/dml-frame_warn_limit.patch deleted file mode 100644 index d3947011..00000000 --- a/internal/rosa/package/kernel/dml-frame_warn_limit.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/drivers/gpu/drm/amd/display/dc/dml/Makefile b/drivers/gpu/drm/amd/display/dc/dml/Makefile -index 268b5fbdb48b..9f1ef07fdfe5 100644 ---- a/drivers/gpu/drm/amd/display/dc/dml/Makefile -+++ b/drivers/gpu/drm/amd/display/dc/dml/Makefile -@@ -36,7 +36,7 @@ ifneq ($(CONFIG_FRAME_WARN),0) - frame_warn_limit := 3072 - endif - else -- frame_warn_limit := 2048 -+ frame_warn_limit := 3072 - endif - - ifeq ($(call test-lt, $(CONFIG_FRAME_WARN), $(frame_warn_limit)),y) diff --git a/internal/rosa/package/kernel/package.az b/internal/rosa/package/kernel/package.az index f2be2890..47bc4323 100644 --- a/internal/rosa/package/kernel/package.az +++ b/internal/rosa/package/kernel/package.az @@ -3,11 +3,10 @@ package kernel-source { website = "https://kernel.org"; exclude = true; - version# = "6.18.49"; + version# = "6.18.57"; output = remoteTar { - url = "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/"+ - "snapshot/linux-"+version+".tar.gz"; - checksum = "J0FT9987a7mlYBGcACXWeTsmfScbS8FM_JAG9LLl917UdlcFRP_UDj73yR82bvGR"; + url = "https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-"+version+".tar.gz"; + checksum = "CnOD4u2pRjkjmyD6wg4dyQVnXXy8rNlwZ-OmTZvSFZPJNSEC996heziLGbKQNCTL"; compress = gzip; }; } @@ -46,8 +45,8 @@ cat \ checksum = arch { default = ""; - amd64 = "uwTZX3iLp6iPsR0-F0Jz2aUviLMMqv4xGQHoKeemo2HrNLD7fDlIDqqYT--i_5HJ"; - arm64 = "A9EWzTGEChVLJVm5BP1GzpibdUddXUN2hIjAGkD-KQr5sc6RuEeIQle8VAiAYUqp"; + amd64 = "8BdskuTo4WxcJDFbdHakHmvkeIASmOkDd0LanTjaXwkmoMa5PeMf0gqee0FqYRFd"; + arm64 = "RQ27XMgxEj5M6cagVHljCE1n-9q_gFayOaakWr0PoAq5yllfLHEz0KQx1fXLwCzp"; riscv64 = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; }; @@ -61,7 +60,6 @@ package kernel { anitya = 389470; source = kernel-source; - patches = [ "dml-frame_warn_limit.patch" ]; files = { ".config": arch { amd64 = "amd64.config"; diff --git a/internal/rosa/package/kernel/version.h b/internal/rosa/package/kernel/version.h index 4fc45286..8b0cd01b 100644 --- a/internal/rosa/package/kernel/version.h +++ b/internal/rosa/package/kernel/version.h @@ -1,5 +1,5 @@ -#define LINUX_VERSION_CODE 397870 +#define LINUX_VERSION_CODE 397881 #define KERNEL_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + ((c) > 255 ? 255 : (c))) #define LINUX_VERSION_MAJOR 6 #define LINUX_VERSION_PATCHLEVEL 18 -#define LINUX_VERSION_SUBLEVEL 46 +#define LINUX_VERSION_SUBLEVEL 57 diff --git a/internal/rosa/package/libarchive/CTestCustom.cmake b/internal/rosa/package/libarchive/CTestCustom.cmake index cd1bc34a..6ec6d952 100644 --- a/internal/rosa/package/libarchive/CTestCustom.cmake +++ b/internal/rosa/package/libarchive/CTestCustom.cmake @@ -61,4 +61,5 @@ list(APPEND CTEST_CUSTOM_TESTS_IGNORE "libarchive_test_read_format_ustar_filename" "libarchive_test_read_append_wrong_filter" "libarchive_test_read_format_zip_ppmd8_aes256_streaming" + "libarchive_test_read_format_zip_lzma_alone_large_size" ) diff --git a/internal/rosa/package/libarchive/package.az b/internal/rosa/package/libarchive/package.az index b06daaf3..bd57a860 100644 --- a/internal/rosa/package/libarchive/package.az +++ b/internal/rosa/package/libarchive/package.az @@ -3,11 +3,11 @@ package libarchive { website = "https://www.libarchive.org"; anitya = 1558; - version# = "3.8.9"; + version# = "3.8.10"; source = remoteGitHub { suffix = "libarchive/libarchive"; tag = "v"+version; - checksum = "8C_8Mr22elOAO8Qvxl-B-R-B35M7dpdFdAlph1CeE1L75JdyUnolee-P3KDRFc8T"; + checksum = "B8gGWvDB24_W4rzlPz1PxQYroCn-xOmUcnhsRVmzYl-L9mV_t6uj-TnpvjGqQjkV"; }; files = { "CTestCustom.cmake"; }; diff --git a/internal/rosa/package/libbpf.az b/internal/rosa/package/libbpf.az index 33df95e4..8c21b7e9 100644 --- a/internal/rosa/package/libbpf.az +++ b/internal/rosa/package/libbpf.az @@ -3,11 +3,11 @@ package libbpf { website = "https://github.com/libbpf/libbpf"; anitya = 141355; - version# = "1.7.0"; + version# = "1.8.0"; source = remoteGitHub { suffix = "libbpf/libbpf"; tag = "v"+version; - checksum = "Gsk3TYDTne6behhdiyRB0Y5GTGECRh2265VY7IQ_VB62vIm7U9oqzEW-Va5Tvi_w"; + checksum = "vpacRQrLgXtttQw0fwv3fcJ-FRHbly38CcLPJyXd2MsKr8gsfp1K6bBVelxZZj52"; }; env = [ "PREFIX=/system" ]; diff --git a/internal/rosa/package/libevdev.az b/internal/rosa/package/libevdev.az index 85c9b674..cd4cd671 100644 --- a/internal/rosa/package/libevdev.az +++ b/internal/rosa/package/libevdev.az @@ -3,12 +3,12 @@ package libevdev { website = "https://www.freedesktop.org/software/libevdev"; anitya = 20540; - version# = "1.13.7"; + version# = "1.14.0"; source = remoteGitLab { domain = "gitlab.freedesktop.org"; suffix = "libevdev/libevdev"; ref = "libevdev-"+version; - checksum = "zmN9kkbYkrwr1YdzeMxlZIuiMkRQ9-mZ9DTcBufMckYWUq2IutSt9ZtRE4Agl75D"; + checksum = "_ybQw43ruWkQQr1uiIfhOKY5HKBKknipWef7EJUhPnTelwkxurGKN2fvEX7Z8EWy"; }; exec = meson { diff --git a/internal/rosa/package/libexpat.az b/internal/rosa/package/libexpat.az index acb8445b..2f6386f5 100644 --- a/internal/rosa/package/libexpat.az +++ b/internal/rosa/package/libexpat.az @@ -3,7 +3,7 @@ package libexpat { website = "https://libexpat.github.io"; anitya = 770; - version# = "2.8.4"; + version# = "2.9.0"; source = remoteGitHubRelease { suffix = "libexpat/libexpat"; tag = "R_"+replace { @@ -12,7 +12,7 @@ package libexpat { new = "_"; }; name = "expat-"+version+".tar.bz2"; - checksum = "Nbi2wl8U9KHf3LVLczfAb5Kqr7HFv_myReVjxKXUkfhToWfL-pP2o44fDpWHJ4ig"; + checksum = "dMHFWTPDk1trundt3hRvg6lKbTTcxN-MCRGph2_rfWNA_HYOURLNmlYm9i25c6zZ"; compress = bzip2; }; diff --git a/internal/rosa/package/libglvnd.az b/internal/rosa/package/libglvnd.az index dbcf198b..9be9e2a9 100644 --- a/internal/rosa/package/libglvnd.az +++ b/internal/rosa/package/libglvnd.az @@ -22,6 +22,7 @@ export DISPLAY=':0' Xvfb & XVFB_PID="$!" trap 'kill $XVFB_PID && wait $XVFB_PID' EXIT +while [ ! -e /tmp/.X11-unix/X0 ]; do sleep 0.1; done `; }; diff --git a/internal/rosa/package/libinput.az b/internal/rosa/package/libinput.az index aeb54f77..3769f680 100644 --- a/internal/rosa/package/libinput.az +++ b/internal/rosa/package/libinput.az @@ -3,12 +3,12 @@ package libinput { website = "https://www.freedesktop.org/wiki/Software/libinput"; anitya = 5781; - version# = "1.31.3"; + version# = "1.32.0"; source = remoteGitLab { domain = "gitlab.freedesktop.org"; suffix = "libinput/libinput"; ref = version; - checksum = "GxBGPN6YybQxrD2MDsIL8gdDYImXn4NAJi6EvTx_Hb_1jcbjwCrjeyjY2upUyTMi"; + checksum = "hjuI9afDPrCORLCCM5koBjyRNmTr_YCUmmF4Tl0bqK_7fz0beC8125wt3lmimtgL"; }; bin = [ "bash" ]; diff --git a/internal/rosa/package/libliftoff/0001-test-Disable-alloc-deadline-on-tests.patch b/internal/rosa/package/libliftoff/0001-test-Disable-alloc-deadline-on-tests.patch new file mode 100644 index 00000000..6747ac01 --- /dev/null +++ b/internal/rosa/package/libliftoff/0001-test-Disable-alloc-deadline-on-tests.patch @@ -0,0 +1,139 @@ +From 343a7d02cf5417e0732d8ba73089f86dd6173253 Mon Sep 17 00:00:00 2001 +From: David Turner <david.turner@raspberrypi.com> +Date: Wed, 5 Mar 2025 16:02:25 +0000 +Subject: [PATCH] test: Disable alloc deadline on tests + +I've been seeing occasional spurious CI failures due to +liftoff_output_apply() deadline timeouts, presumably when the runner is +busy. Disable the allocation deadline in all tests to stop this +happening. +--- + test/test_alloc.c | 16 ++++++++++++---- + test/test_candidate.c | 4 +++- + test/test_dynamic.c | 8 ++++++-- + test/test_priority.c | 4 +++- + test/test_prop.c | 4 +++- + 5 files changed, 27 insertions(+), 9 deletions(-) + +diff --git a/test/test_alloc.c b/test/test_alloc.c +index 8d0caf7..a002c46 100644 +--- a/test/test_alloc.c ++++ b/test/test_alloc.c +@@ -803,7 +803,9 @@ run_test(const struct test_case *test) + } + + req = drmModeAtomicAlloc(); +- ret = liftoff_output_apply(output, req, 0, NULL); ++ ret = liftoff_output_apply(output, req, 0, &(struct liftoff_output_apply_options){ ++ .timeout_ns = INT64_MAX, ++ }); + assert(ret == 0); + ret = drmModeAtomicCommit(drm_fd, req, 0, NULL); + assert(ret == 0); +@@ -875,7 +877,9 @@ test_basic(void) + liftoff_mock_plane_add_compatible_layer(mock_plane, layer); + + req = drmModeAtomicAlloc(); +- ret = liftoff_output_apply(output, req, 0, NULL); ++ ret = liftoff_output_apply(output, req, 0, &(struct liftoff_output_apply_options){ ++ .timeout_ns = INT64_MAX, ++ }); + assert(ret == 0); + ret = drmModeAtomicCommit(drm_fd, req, 0, NULL); + assert(ret == 0); +@@ -913,7 +917,9 @@ test_no_props_fail(void) + liftoff_mock_plane_add_compatible_layer(mock_plane, layer); + + req = drmModeAtomicAlloc(); +- ret = liftoff_output_apply(output, req, 0, NULL); ++ ret = liftoff_output_apply(output, req, 0, &(struct liftoff_output_apply_options){ ++ .timeout_ns = INT64_MAX, ++ }); + assert(ret == 0); + ret = drmModeAtomicCommit(drm_fd, req, 0, NULL); + assert(ret == 0); +@@ -959,7 +965,9 @@ test_composition_no_props(void) + liftoff_mock_plane_add_compatible_layer(mock_plane, layer_with_fb); + + req = drmModeAtomicAlloc(); +- ret = liftoff_output_apply(output, req, 0, NULL); ++ ret = liftoff_output_apply(output, req, 0, &(struct liftoff_output_apply_options){ ++ .timeout_ns = INT64_MAX, ++ }); + assert(ret == 0); + ret = drmModeAtomicCommit(drm_fd, req, 0, NULL); + assert(ret == 0); +diff --git a/test/test_candidate.c b/test/test_candidate.c +index 5e7f698..74a0bcb 100644 +--- a/test/test_candidate.c ++++ b/test/test_candidate.c +@@ -60,7 +60,9 @@ test_basic(void) + liftoff_layer_set_property(layer, "COLOR_RANGE", 0); + + req = drmModeAtomicAlloc(); +- ret = liftoff_output_apply(output, req, 0, NULL); ++ ret = liftoff_output_apply(output, req, 0, &(struct liftoff_output_apply_options){ ++ .timeout_ns = INT64_MAX, ++ }); + assert(ret == 0); + ret = drmModeAtomicCommit(drm_fd, req, 0, NULL); + assert(ret == 0); +diff --git a/test/test_dynamic.c b/test/test_dynamic.c +index 3a95a7f..8c1049d 100644 +--- a/test/test_dynamic.c ++++ b/test/test_dynamic.c +@@ -50,7 +50,9 @@ first_commit(struct context *ctx) + assert(ctx->commit_count == 0); + + req = drmModeAtomicAlloc(); +- ret = liftoff_output_apply(ctx->output, req, 0, NULL); ++ ret = liftoff_output_apply(ctx->output, req, 0, &(struct liftoff_output_apply_options){ ++ .timeout_ns = INT64_MAX, ++ }); + assert(ret == 0); + ret = drmModeAtomicCommit(ctx->drm_fd, req, 0, NULL); + assert(ret == 0); +@@ -72,7 +74,9 @@ second_commit(struct context *ctx, bool want_reuse_prev_alloc) + int ret; + + req = drmModeAtomicAlloc(); +- ret = liftoff_output_apply(ctx->output, req, 0, NULL); ++ ret = liftoff_output_apply(ctx->output, req, 0, &(struct liftoff_output_apply_options){ ++ .timeout_ns = INT64_MAX, ++ }); + assert(ret == 0); + if (want_reuse_prev_alloc) { + /* The library should perform only one TEST_ONLY commit with the +diff --git a/test/test_priority.c b/test/test_priority.c +index cf2e4c5..41ede18 100644 +--- a/test/test_priority.c ++++ b/test/test_priority.c +@@ -76,7 +76,9 @@ main(int argc, char *argv[]) + + liftoff_layer_set_property(layer, "FB_ID", fbs[j % 2]); + +- ret = liftoff_output_apply(output, req, 0, NULL); ++ ret = liftoff_output_apply(output, req, 0, &(struct liftoff_output_apply_options){ ++ .timeout_ns = INT64_MAX, ++ }); + assert(ret == 0); + ret = drmModeAtomicCommit(drm_fd, req, 0, NULL); + assert(ret == 0); +diff --git a/test/test_prop.c b/test/test_prop.c +index 05193c7..093b581 100644 +--- a/test/test_prop.c ++++ b/test/test_prop.c +@@ -34,7 +34,9 @@ commit(int drm_fd, struct liftoff_output *output) + int ret; + + req = drmModeAtomicAlloc(); +- ret = liftoff_output_apply(output, req, 0, NULL); ++ ret = liftoff_output_apply(output, req, 0, &(struct liftoff_output_apply_options){ ++ .timeout_ns = INT64_MAX, ++ }); + assert(ret == 0); + ret = drmModeAtomicCommit(drm_fd, req, 0, NULL); + assert(ret == 0); +-- +2.55.0 + diff --git a/internal/rosa/package/libliftoff.az b/internal/rosa/package/libliftoff/package.az index a1c23fd8..7d8900bc 100644 --- a/internal/rosa/package/libliftoff.az +++ b/internal/rosa/package/libliftoff/package.az @@ -10,6 +10,7 @@ package libliftoff { ref = "v"+version; checksum = "anxKfrW56iK7l6-bm6RGcaC_uk-Kc8x3_VqKaywmSjv5HLCs6A2sHyjGUbOC9xZv"; }; + patches = [ "0001-test-Disable-alloc-deadline-on-tests.patch" ]; exec = meson {}; diff --git a/internal/rosa/package/libmd.az b/internal/rosa/package/libmd.az index e618d11e..f8fafd80 100644 --- a/internal/rosa/package/libmd.az +++ b/internal/rosa/package/libmd.az @@ -3,11 +3,11 @@ package libmd { website = "https://www.hadrons.org/software/libmd"; anitya = 15525; - version# = "1.2.0"; + version# = "1.3.0"; source = remoteGit { url = "https://git.hadrons.org/git/libmd.git"; tag = version; - checksum = "1rJ6joAO0wwMZvSfnRNkc1MOhywyAq7SM8VmF92NvDtv7Qdl1LRbjm5fg_DFFtGj"; + checksum = "CuzJwaavZusUsVcQEAJDsQ2mf9C9BjFhYLemSt0y6K_Gkx0niacVjqV7QpjFjhta"; }; exec = make { diff --git a/internal/rosa/package/libpng.az b/internal/rosa/package/libpng.az index 4711cbce..431c38a4 100644 --- a/internal/rosa/package/libpng.az +++ b/internal/rosa/package/libpng.az @@ -3,7 +3,7 @@ package libpng { website = "https://www.libpng.org/pub/png/libpng.html"; anitya = 1705; - version# = "1.6.58"; + version# = "1.6.59"; source = remoteTar { url = "https://downloads.sourceforge.net/project/libpng/libpng"+ join { @@ -16,7 +16,7 @@ package libpng { end = 2; }; }+"/"+version+"/libpng-"+version+".tar.gz"; - checksum = "m_a5lROJH7vmF3cMjqwTUqURuQLhV1JQx2ySPzcN3VPdgDB9pG3UINsIx_mtkr-t"; + checksum = "W-glb0kwxxmfjQepn0bFejBtMZXCiRM0OQ_I-TvyigQr_EmMBEI44WBG-3uCWbIK"; compress = gzip; }; diff --git a/internal/rosa/package/libtirpc.az b/internal/rosa/package/libtirpc.az index 0b12a92e..b1a49c79 100644 --- a/internal/rosa/package/libtirpc.az +++ b/internal/rosa/package/libtirpc.az @@ -3,7 +3,7 @@ package libtirpc { website = "https://sourceforge.net/projects/libtirpc"; anitya = 1740; - version# = "1.3.7"; + version# = "1.3.8"; source = remoteGit { url = "git://linux-nfs.org/~steved/libtirpc"; tag = "libtirpc-"+join { @@ -14,7 +14,7 @@ package libtirpc { }; sep = "-"; }; - checksum = "nzFfu7LNvnSNiNAryD1vtnNWnU-Xqee8KqfXUKoBf5yjb5-dkeRkYuRijdCoYLof"; + checksum = "OYM7CljjvlHv3Xzc9bXC2WRfBQY0QcBEZ9ubE8qVDweq9ZvQPrAWxSOFVhFBoToz"; }; exec = make { @@ -29,6 +29,10 @@ package libtirpc { automake, libtool, pkgconf, + + // version script assignment of 'TIRPC_1.3.7' to symbol 'authdes_getucred' failed: symbol not defined + // version script assignment of 'TIRPC_1.3.7' to symbol 'rpc_gss_getcred' failed: symbol not defined + binutils, ]; inputs = [ diff --git a/internal/rosa/package/llvm/package.az b/internal/rosa/package/llvm/package.az index 11210563..05a9d937 100644 --- a/internal/rosa/package/llvm/package.az +++ b/internal/rosa/package/llvm/package.az @@ -2,11 +2,11 @@ package llvm-project { description = "LLVM monorepo with Rosa OS patches"; anitya = 1830; - version# = "23.1.0"; + version# = "23.1.3"; output = remoteGitHub { suffix = "llvm/llvm-project"; tag = "llvmorg-"+version; - checksum = "wgirRNAhU9451qsZn4FXcaIButgRNZWb4kb8pQwERyF1PGc-iJQVewr6xGk7ctNj"; + checksum = "bEWWfej4llsLsQ_wKIv57zcfNNgQD1yihufRDeSDYDlTzyrMTUS8J-ve6Bk2kPrJ"; }; patches = [ "increase-stack-size-unconditional.patch", diff --git a/internal/rosa/package/lvm2/package.az b/internal/rosa/package/lvm2/package.az index 3a363b54..cc0ccef3 100644 --- a/internal/rosa/package/lvm2/package.az +++ b/internal/rosa/package/lvm2/package.az @@ -3,11 +3,11 @@ package lvm2 { website = "https://sourceware.org/lvm2"; anitya = 5354; - version# = "2.03.42"; + version# = "2.03.43"; source = remoteTar { url = "https://mirrors.kernel.org/sourceware/lvm2/releases/LVM2."+version+".tgz"; compress = gzip; - checksum = "qzqVAeW9ttt_H5W0Wfg8twvZjFZH7x_RIzSjqYSooMsRpiXQ7yNdOL1MXY5Zntvi"; + checksum = "W98OpJpTiLc-wO7CHh08UQ8nc-gNEj3kr84IzOv2QDoYfnonVVj-Lt_NZLJi_vi5"; }; patches = [ "fix-stdio-usage.patch", diff --git a/internal/rosa/package/mesa/include.patch b/internal/rosa/package/mesa/include.patch index b1942625..4a8a7fe9 100644 --- a/internal/rosa/package/mesa/include.patch +++ b/internal/rosa/package/mesa/include.patch @@ -1,15 +1,3 @@ -diff --git a/src/amd/compiler/aco_util.h b/src/amd/compiler/aco_util.h -index 82d754f155a..9f20e80a554 100644 ---- a/src/amd/compiler/aco_util.h -+++ b/src/amd/compiler/aco_util.h -@@ -12,6 +12,7 @@ - #include "util/macros.h" - #include "util/u_math.h" - -+#include <algorithm> - #include <array> - #include <cassert> - #include <cstddef> diff --git a/src/freedreno/common/freedreno_common.h b/src/freedreno/common/freedreno_common.h index 36dea4e12b5..cfa243c270f 100644 --- a/src/freedreno/common/freedreno_common.h diff --git a/internal/rosa/package/mesa/package.az b/internal/rosa/package/mesa/package.az index 8db6462e..b15bb880 100644 --- a/internal/rosa/package/mesa/package.az +++ b/internal/rosa/package/mesa/package.az @@ -4,12 +4,12 @@ package mesa { anitya = 1970; latest = anityaFallback; - version# = "26.2.2"; + version# = "26.2.5"; source = remoteGitLab { domain = "gitlab.freedesktop.org"; suffix = "mesa/mesa"; ref = "mesa-"+version; - checksum = "5mIdojhazcdAgbLW1v4d-7P5pIhu9wnGS3ek1QhJIPMJyFWhfKhiZCsd53WAHt1_"; + checksum = "uGxr-ED_AydefaUMuVWnIFtT6F8COqbKvQbmsVwKn0VKEZL-nbUHuvHvP-YuCr5v"; }; patches = [ "include.patch" ]; diff --git a/internal/rosa/package/meson/package.az b/internal/rosa/package/meson/package.az index 78fa2035..9b266654 100644 --- a/internal/rosa/package/meson/package.az +++ b/internal/rosa/package/meson/package.az @@ -3,11 +3,11 @@ package meson { website = "https://mesonbuild.com"; anitya = 6472; - version# = "1.12.0"; + version# = "1.12.1"; source = remoteGitHub { suffix = "mesonbuild/meson"; tag = version; - checksum = "K0hm0h26_6e3-1poYZKwj18IB6bdAtYE-whpROtTCSpVN5GllwSwbXC2xPYcSFAJ"; + checksum = "P7lvRR-dRtSmNF4CZtVWd6uwGPzjkE76MvJL0s3DDw-CFparOmUK1DNEkCt-cuEF"; }; patches = [ "respect-colour.patch", diff --git a/internal/rosa/package/muon.az b/internal/rosa/package/muon/package.az index dc183591..3a5be8bd 100644 --- a/internal/rosa/package/muon.az +++ b/internal/rosa/package/muon/package.az @@ -3,13 +3,14 @@ package meson-tests { website = "https://github.com/muon-build/meson-tests"; exclude = true; - version# = "0bbaa42e7ba553356fbec038e8b40cf89f90da82"; + version# = "38483b8cf5eb4f6e9df7a78d786ec111e0d6eb5d"; output = remoteTar { url = "https://github.com/muon-build/meson-tests/archive/"+ version+".tar.gz"; - checksum = "W_yNC2Id1aGBXLmBhrQ53UARVBl-nZ2bDZJcmZDaE4GyoZyj6tSURojuTyWgG1U8"; + checksum = "ikrJiczlObaLUMHRbzzWmGgAyZMymxxC3D9VFozD0sV3e5ckiHXk8qk0Zr8bBWvO"; compress = gzip; }; + patches = [ "remove-broken-tests.patch" ]; } package muon { @@ -17,12 +18,19 @@ package muon { website = "https://muon.build"; anitya = 391173; - version# = "0.6.0"; + version# = "0.7.0"; source = remoteTar { url = "https://git.sr.ht/~lattis/muon/archive/"+version+".tar.gz"; - checksum = "DiRw1dTaHlL5d3TNK8gykFHI_BAWK7ERooSmVC-kltUkjOubWkN9b-DG21a1ETCe"; + checksum = "ackLkMhxutd7xvLAVU25qkM5CAztLaYf1dAC2vIvlRIZ8o6LjaNpH-3OyP1xGQTO"; compress = gzip; }; + patches = [ + // for some reason, muon decides to use clang-cl on Rosa OS + "remove-cl.patch", + + // equivalent to the meson patches + "respect-colour.patch", + ]; extra = [ meson-tests ]; writable = true; @@ -30,7 +38,6 @@ package muon { chmod +w subprojects cp -r ../extra/meson-tests subprojects chmod -R +w subprojects/meson-tests -sed -i '/common.220 fs module/d' subprojects/meson-tests/meson.build `; exec = generic { diff --git a/internal/rosa/package/muon/remove-broken-tests.patch b/internal/rosa/package/muon/remove-broken-tests.patch new file mode 100644 index 00000000..d4f868be --- /dev/null +++ b/internal/rosa/package/muon/remove-broken-tests.patch @@ -0,0 +1,30 @@ +diff --git a/meson.build b/meson.build +index ebcd241..30b4c57 100644 +--- a/meson.build ++++ b/meson.build +@@ -213,7 +213,6 @@ foreach t : [ + ['common/202 custom target build by default', {'python': true}], + ['common/203 find_library and headers'], + ['common/204 line continuation'], +- ['common/205 native file path override', {'cpp': true}], + ['common/206 tap tests'], + ['common/207 warning level 0', {'cpp': true}], + ['common/208 link custom', {'python': true}], +@@ -228,9 +227,7 @@ foreach t : [ + ['common/217 test priorities', {'python': true}], + ['common/218 include_dir dot'], + ['common/219 include_type dependency', {'cpp': true}], +- ['common/220 fs module'], + ['common/221 zlib'], +- ['common/222 native prop'], + ['common/223 persubproject options', {'cpp': true}], + ['common/224 arithmetic operators'], + ['common/225 link language', {'cpp': true}], +@@ -336,7 +333,6 @@ foreach t : [ + ['common/280 pkgconfig override', {'python': true}], + ['common/281 pkgconfig-gen'], + ['common/282 -D_FORTIFY_SOURCE=2 and -O0'], +- ['common/283 subproj options', {'options': {'b_lto': 'true'}}], + ['common/284 test args and depends in path'], + ['common/285 wrap override'], + ['common/286 pkgconfig subproject'], diff --git a/internal/rosa/package/muon/remove-cl.patch b/internal/rosa/package/muon/remove-cl.patch new file mode 100644 index 00000000..e421c25d --- /dev/null +++ b/internal/rosa/package/muon/remove-cl.patch @@ -0,0 +1,39 @@ +diff --git a/src/script/runtime/toolchains.meson b/src/script/runtime/toolchains.meson +index 97682251..bb49b041 100644 +--- a/src/script/runtime/toolchains.meson ++++ b/src/script/runtime/toolchains.meson +@@ -1059,22 +1059,6 @@ toolchain.register_compiler( + }, + ) + +-toolchain.register_compiler( +- 'clang-cl', +- exe: {'c': 'clang-cl', 'cpp': 'clang-cl'}, +- inherit: 'msvc', +- linker: 'lld-link', +- archiver: 'llvm-lib', +- detect: func(out str) -> int +- return 'CL.EXE COMPATIBILITY' in out ? 100 : 0 +- endfunc, +- handlers: { +- 'force_language': func(_c compiler, s str) -> list[str] +- return ['-x', s] +- endfunc, +- }, +-) +- + toolchain.register_compiler( + 'nasm', + exe: {'nasm': 'nasm'}, +diff --git a/src/vsenv.c b/src/vsenv.c +index f55541ec..c92e665a 100644 +--- a/src/vsenv.c ++++ b/src/vsenv.c +@@ -73,7 +73,6 @@ vsenv_setup(struct workspace *wk, const char *cache_path, enum setup_platform_en + "cc", + "gcc", + "clang", +- "clang-cl", + }; + uint32_t i; + for (i = 0; i < ARRAY_LEN(comps); ++i) { diff --git a/internal/rosa/package/muon/respect-colour.patch b/internal/rosa/package/muon/respect-colour.patch new file mode 100644 index 00000000..a4da13d5 --- /dev/null +++ b/internal/rosa/package/muon/respect-colour.patch @@ -0,0 +1,14 @@ +diff --git a/src/log.c b/src/log.c +index 34f11f19..22fe8f7d 100644 +--- a/src/log.c ++++ b/src/log.c +@@ -506,6 +506,9 @@ log_set_file(struct workspace *wk, FILE *log_file) + log_cfg.tstr = 0; + log_cfg.tstr_wk = 0; + log_cfg.file_is_a_tty = log_file && fs_is_a_tty(wk, log_file); ++ if (getenv("CLICOLOR_FORCE") != NULL) { ++ log_cfg.file_is_a_tty = true; ++ } + } + + void diff --git a/internal/rosa/package/noto.az b/internal/rosa/package/noto.az index 7481b5e7..6fc8e125 100644 --- a/internal/rosa/package/noto.az +++ b/internal/rosa/package/noto.az @@ -3,11 +3,11 @@ package noto { website = "https://fonts.google.com/noto"; anitya = 10671; - version# = "2026.09.01"; + version# = "2026.10.01"; source = remoteGitHub { suffix = "notofonts/notofonts.github.io"; tag = "noto-monthly-release-"+version; - checksum = "ZITq5IyZNZovca3t7snPndDgjdzbCeBIoGYpY7bbnR-w9oAiz1vkN9zwRWz5JJw5"; + checksum = "lEKVFkRavG8j1wNdrVFK6s1k8Tlp3ICN30cIX_tuTv5w6uAnAMDIWSoviJ4oiwu8"; }; enterSource = true; diff --git a/internal/rosa/package/nss.az b/internal/rosa/package/nss.az index fb9c1a33..115e5667 100644 --- a/internal/rosa/package/nss.az +++ b/internal/rosa/package/nss.az @@ -3,7 +3,7 @@ package nss-cacert { website = "https://firefox-source-docs.mozilla.org/security/nss/index.html"; anitya = 2503; - version# = "3.128"; + version# = "3.131"; source = remoteGitHub { suffix = "nss-dev/nss"; tag = "NSS_"+join { @@ -14,7 +14,7 @@ package nss-cacert { }; sep = "_"; }+"_RTM"; - checksum = "uVP9Tbv17Vo7tUAm1LK16vu-9PA4Mecjc1Kr6y4ptXby9e0zT7I15rzsD2fMApqA"; + checksum = "fQZ1hbSQyeqc2uK1bx4CeYPb3nb1t4M40D3_JXOJ0WO6a0QvMtddn2jQjKmnmCKD"; }; enterSource = true; diff --git a/internal/rosa/package/openssl.az b/internal/rosa/package/openssl.az index cb76d0fa..eb2f28fa 100644 --- a/internal/rosa/package/openssl.az +++ b/internal/rosa/package/openssl.az @@ -5,14 +5,12 @@ package openssl { // strange malformed tags treated as pre-releases in Anitya latest = anityaFallback; - block = "python"; - - version# = "3.6.2"; + version# = "4.0.3"; source = remoteGitHubRelease { suffix = "openssl/openssl"; tag = "openssl-"+version; name = "openssl-"+version+".tar.gz"; - checksum = "jH004dXTiE01Hp0kyShkWXwrSHEksZi4i_3v47D9H9Uz9LQ1aMwF7mrl2Tb4t_XA"; + checksum = "qRm2EpUnLqKBgUFimF2uqjHZJaUgK-AWTuX9DVHhnowvCdm09p0wIn3mPRjXjGxj"; compress = gzip; }; diff --git a/internal/rosa/package/pcre2.az b/internal/rosa/package/pcre2.az index 85f603a9..1df3fdad 100644 --- a/internal/rosa/package/pcre2.az +++ b/internal/rosa/package/pcre2.az @@ -3,12 +3,12 @@ package pcre2 { website = "https://pcre2project.github.io/pcre2"; anitya = 5832; - version# = "10.48"; + version# = "10.49"; source = remoteGitHubRelease { suffix = "PCRE2Project/pcre2"; tag = "pcre2-"+version; name = "pcre2-"+version+".tar.bz2"; - checksum = "3HorftFDryXBh8JLSbEVWpU3OtSF_wUhtNZNCRvCDiGlBsMU150CxrDFKE8lVo6x"; + checksum = "3vFs-Cz17RWuneRKbt76ZTGgyqxMWDm9gXV9P7eIh4IaTYgyi2XTvdU3dAeJrB7T"; compress = bzip2; }; diff --git a/internal/rosa/package/pipewire.az b/internal/rosa/package/pipewire.az index 06153a9f..230755fd 100644 --- a/internal/rosa/package/pipewire.az +++ b/internal/rosa/package/pipewire.az @@ -3,12 +3,12 @@ package wireplumber { website = "https://gitlab.freedesktop.org/pipewire/wireplumber"; anitya = 235056; - version# = "0.5.17"; + version# = "0.5.18"; source = remoteGitLab { domain = "gitlab.freedesktop.org"; suffix = "pipewire/wireplumber"; ref = version; - checksum = "2vWxpOqnQmOlgMlNLYMu9ebPbF2y3W_eqDFM_bwAtCwnHhYE0S4kM8MkpFuXyVlu"; + checksum = "YsKFuMxDJQtwnR0N7h7KA7fNR866k7XhCiSdn3L30Wn1kU7mTV66V8eHHL9dZ5sZ"; }; // OSError: [Errno 30] Read-only file system: '../../usr/src/wireplumber/po/conf.pot' @@ -37,12 +37,12 @@ package pipewire { website = "https://pipewire.org"; anitya = 57357; - version# = "1.6.8"; + version# = "1.6.9"; source = remoteGitLab { domain = "gitlab.freedesktop.org"; suffix = "pipewire/pipewire"; ref = version; - checksum = "KPPJDODFi6RpNpoGztQGNTiINRubQ_7WVIngsboHQi0XzEhMAKVp7iaUG74QX3aE"; + checksum = "EtqI-7Gwgp_6rHRqVlhuGAt39vOlX_SAoUWzaeeCaUqX4X6feCQvqmSwQvzR88Dr"; }; exec = meson { diff --git a/internal/rosa/package/pkgconf.az b/internal/rosa/package/pkgconf.az index 1ff20388..644c916d 100644 --- a/internal/rosa/package/pkgconf.az +++ b/internal/rosa/package/pkgconf.az @@ -3,11 +3,11 @@ package pkgconf { website = "https://github.com/pkgconf/pkgconf"; anitya = 12753; - version# = "3.0.7"; + version# = "3.0.9"; source = remoteGitHub { suffix = "pkgconf/pkgconf"; tag = "pkgconf-"+version; - checksum = "_IpcXySlbiPfFIS95oAg9C65_wV3BAoLD_QVP2OHx4MNTgdndsgT_WjtJkCbhlEd"; + checksum = "uGY_4freweuu58B5ftn9b9fmvND_BN1B6zCmmnFh2nO14aGZ98tCa3qAWakLF3q3"; }; exec = meson { diff --git a/internal/rosa/package/python/package.az b/internal/rosa/package/python/package.az index aef3ecda..fa12b203 100644 --- a/internal/rosa/package/python/package.az +++ b/internal/rosa/package/python/package.az @@ -6,11 +6,11 @@ package python { // too many inputs; early variant available bootstrap = false; - version# = "3.14.7"; + version# = "3.15.0"; source = remoteTar { url = "https://www.python.org/ftp/python/"+version+ "/Python-"+version+".tgz"; - checksum = "hXLV7_BGEFZvQ04DNIlk5IdoRZd9DkQt4abKhc8rWsLXHTeSboAWTewJ6l0-LxVz"; + checksum = "SHy8bsXI1zLONLJHIK6gEJUiPjdUcRLzI9PjQnUfXOdL6Jcy0RQFEOyhMkp9vonP"; compress = gzip; }; patches = [ "zipfile-no-default-strict_timestamps.patch" ]; @@ -41,6 +41,9 @@ package python { // breaks on llvm "test_dbm_gnu", + + // broken on almost everything other than amd64 + "test_c_stack_unwind", ]; sep = " -x "; }, @@ -167,6 +170,7 @@ package python-wheel { tag = version; checksum = "h-UZQHS-NwB3b9YxYpJePR5cbDGZaKxz4bdhYjilWdQXAE46SV7A1CwZKzZcuZFh"; }; + patches = [ "wheel-test-colour.patch" ]; exec = pip { install = true; @@ -182,12 +186,13 @@ package python-vcs-versioning { description = "core VCS versioning functionality extracted as a standalone library"; website = "https://setuptools-scm.readthedocs.io/en/latest"; anitya = 389421; + block = "frequent ineffectual updates; questionable maintainer decisions"; - version# = "2.3.4"; + version# = "2.4.0"; source = remoteGitHub { suffix = "pypa/setuptools-scm"; tag = "vcs-versioning-v"+version; - checksum = "TljjSQUxo07jMzfaVyieXD8dS_olgwEA2_RndGeIjlsdKWSa2oYZ5laMRjdgnXJS"; + checksum = "Ziau4BBAwjQVEVe-MlKpA-27nqOm30LlGfHG5SqIrqKbCABWBndCwzpXLxdnZuGn"; }; env = [ @@ -211,12 +216,13 @@ package python-setuptools-scm { description = "extracts Python package versions from Git or Mercurial metadata"; website = "https://setuptools-scm.readthedocs.io/en/latest"; anitya = 7874; + block = "see package python-vcs-versioning"; - version# = "10.2.3"; + version# = "10.3.2"; source = remoteGitHub { suffix = "pypa/setuptools-scm"; tag = "setuptools-scm-v"+version; - checksum = "C76_jiIc_XanEZrpxLb47B3YArtmlt7HcpEOo0wZ5Z4mCFJb2HLIgOSOiRSfttpW"; + checksum = "wnOU9_VeAiNp881loQiJD3KuWgbufNXhQKqHOGBdt9oEjIMXosHjUseE67fW4ips"; }; env = [ @@ -248,11 +254,11 @@ package python-flit-core { website = "https://flit.pypa.io"; anitya = 44841; - version# = "4.0.2"; + version# = "4.1.0"; source = remoteGitHub { suffix = "pypa/flit"; tag = version; - checksum = "LAYWMeQ2wf2fUavUctRHlEVCUUfsVu0so4d0DZh-DAkAvd8kf9ZHQd-o7dXr3tq6"; + checksum = "c-iUN-XyUB-1jF4CYaQaMNa5W0G4c3UcpjQcwSI9KOZFQ_my3ebpPjBvfVchjywC"; }; exec = pip { @@ -310,11 +316,11 @@ package python-trove-classifiers { website = "https://pypi.org/p/trove-classifiers"; anitya = 88298; - version# = "2026.6.1.19"; + version# = "2026.9.21.13"; source = remoteGitHub { suffix = "pypa/trove-classifiers"; tag = version; - checksum = "zdDI6XTfzuFnlhdhilnhplItbLhZ9f2_vSHZP8u5Zvhu5_rhTB73SFEzxT5gb1bO"; + checksum = "SUj49aPO71eUQFxBE-G_BuJezwEUfeYCv9RbWFFsyTGmfbLGsZelEvqXGHErX-sZ"; }; exec = pip { @@ -330,11 +336,11 @@ package python-pluggy { website = "https://pluggy.readthedocs.io/en/latest"; anitya = 7500; - version# = "1.6.0"; + version# = "1.7.0"; source = remoteGitHub { suffix = "pytest-dev/pluggy"; tag = version; - checksum = "GiUgDkKjF8Xn1cmq6iMhTGXzcPIYeaJrvQpHBSAJapNVx4UyuiTXqd5eVlxSClJu"; + checksum = "KukKDsVCm3aO-eMHI0fN2LCN5qU8wVUo-95T7MuRf_DOmXRNDiIuaDeWrIQxd0-h"; }; env = [ @@ -354,11 +360,11 @@ package python-poetry-core { website = "https://github.com/python-poetry/poetry-core"; anitya = 71155; - version# = "2.4.1"; + version# = "2.5.0"; source = remoteGitHub { suffix = "python-poetry/poetry-core"; tag = version; - checksum = "tf9OeSUv9TJwKd48fQIU2uGtZ36GN-PDmPDZzjClozzLjv-rQ5nIbOg9gOXpegiH"; + checksum = "FZCJeScQLZBm2W1JiqgPozV2QKxkU0Dr0tQDsFf-H3PNswQwxSLPEGgEl94va7r3"; }; exec = pip { @@ -374,11 +380,11 @@ package python-tomlkit { website = "https://github.com/python-poetry/tomlkit"; anitya = 19609; - version# = "0.15.1"; + version# = "0.15.2"; source = remoteGitHub { suffix = "python-poetry/tomlkit"; tag = version; - checksum = "6n8h0Yf-OO0ARildxqyB1mB1EhS7uALtqFJqD7t7G1JSFTTb9RGRWSpCg-vamuu8"; + checksum = "f7Cwnf-mzMPzRoSNkRikLxQ3TXzlzbx69K7oSpDmWfUwD8rlR-hy3ZLOKuFI2G6i"; }; exec = pip { @@ -394,11 +400,11 @@ package python-hatchling { website = "https://hatch.pypa.io"; anitya = 16137; - version# = "1.18.0"; + version# = "1.18.1"; source = remoteGitHub { suffix = "pypa/hatch"; tag = "hatch-v"+version; - checksum = "a_ntndxBlMdqSVW3PkiAY42m-NDgyWJcKLWFIVTB_5PciVeJdYYdbcdF6jsd0U96"; + checksum = "MQHbzLg0Vo9Wp8oNwFqzyr6Z8xIqI8uxp1r_sue5r_QMI6Ryfx4wXJWpQQgdmUdJ"; }; exec = pip { @@ -444,11 +450,11 @@ package python-iniconfig { website = "https://github.com/pytest-dev/iniconfig"; anitya = 114778; - version# = "2.3.0"; + version# = "2.3.1"; source = remoteGitHub { suffix = "pytest-dev/iniconfig"; tag = "v"+version; - checksum = "mH7VBZaXcYatBPE3RQQZvSzz_Ay8IPPek60NpPHZulPq4ReAFUUsA4EPWfiyMknZ"; + checksum = "_kAzeTxeje5L7WYtFwMGOx-Qv2pfErrBvoajcz1JovWaeIQ72w94yD5oidld2OxN"; }; env = [ @@ -499,11 +505,11 @@ package python-markupsafe { website = "https://markupsafe.palletsprojects.com"; anitya = 3918; - version# = "3.0.3"; + version# = "3.0.4"; source = remoteGitHub { suffix = "pallets/markupsafe"; tag = version; - checksum = "txRGYdWE3his1lHHRI-lZADw0-ILvUg2l5OGdFHtFXIb_QowGxwdxHCUSJIgmjQs"; + checksum = "V0lPnpENthlPMRq08uHN03BAeqHeZfi_tKgn9oUGGKrz9yMfcgeoUmhP3JT1uhlB"; }; exec = pip { @@ -519,7 +525,7 @@ package python-mako { website = "https://www.makotemplates.org"; anitya = 3915; - version# = "1.4.1"; + version# = "1.4.3"; source = remoteGitHub { suffix = "sqlalchemy/mako"; tag = "rel_"+join { @@ -530,7 +536,7 @@ package python-mako { }; sep = "_"; }; - checksum = "OYVVx9PxvZLSvA1C-lX-xElq5TvThYRElzDDKUHYU1tZMM7XlCtHSMGmdgqlbYxd"; + checksum = "-7wYBvuaMaRtWEPlPbIyfrrVSJWsuNnVr1rCwaXnwNNvyap3R9kL282cLYidO_LX"; }; early = "rm test/test_exceptions.py"; @@ -568,11 +574,11 @@ package python-pycparser { website = "https://github.com/eliben/pycparser"; anitya = 8175; - version# = "3.00"; + version# = "3.11"; source = remoteGitHub { suffix = "eliben/pycparser"; tag = "release_v"+version; - checksum = "4qfCMFKp0fLsRsloOAF780tXX_Ce_68RwinCmjNGObAX32WpF_iBafIKW1S1bYlA"; + checksum = "MjkTN3iAR3UglDqTHUjGozSowe7i0QJWBmaRYygYCH1TYlfFYyCaLx1LKG-Zf5_l"; }; // test case hard codes gcc diff --git a/internal/rosa/package/python/wheel-test-colour.patch b/internal/rosa/package/python/wheel-test-colour.patch new file mode 100644 index 00000000..db399fdc --- /dev/null +++ b/internal/rosa/package/python/wheel-test-colour.patch @@ -0,0 +1,21 @@ +diff --git a/tests/commands/util.py b/tests/commands/util.py +index 5d139d9..d18d103 100644 +--- a/tests/commands/util.py ++++ b/tests/commands/util.py +@@ -2,7 +2,7 @@ from __future__ import annotations + + import sys + from io import StringIO +-from os import PathLike ++from os import PathLike, environ + from subprocess import CalledProcessError + from unittest.mock import patch + +@@ -10,6 +10,7 @@ import pytest + + from wheel._commands import main + ++environ["NO_COLOR"] = "1" + + def run_command( + command: str, *args: str | PathLike, catch_systemexit: bool = True diff --git a/internal/rosa/package/qemu/package.az b/internal/rosa/package/qemu/package.az index 3e7b8fcf..7252c4d8 100644 --- a/internal/rosa/package/qemu/package.az +++ b/internal/rosa/package/qemu/package.az @@ -3,10 +3,10 @@ package qemu { website = "https://www.qemu.org"; anitya = 13607; - version# = "11.1.1"; + version# = "11.1.2"; source = remoteTar { url = "https://download.qemu.org/qemu-"+version+".tar.bz2"; - checksum = "zvoYXpZGfnaD1bo2fD44_GfViIVCCH1iSH0xjy9VAB7Ta1X5AzBtnMnbalc_WEmm"; + checksum = "PA9KeH0ifsIUJDTEq0_zLiysF9nlQXUTQsEx7cRzJiC1P4-u-AKCD-uanbyVASMJ"; compress = bzip2; }; patches = [ diff --git a/internal/rosa/package/spirv.az b/internal/rosa/package/spirv.az index 79ac5a8d..8cfb33f7 100644 --- a/internal/rosa/package/spirv.az +++ b/internal/rosa/package/spirv.az @@ -5,11 +5,11 @@ package spirv-headers { // upstream changed version scheme, anitya incapable of filtering them latest = anityaFilterSPIRV; - version# = "1.4.357.0"; + version# = "1.4.363.0"; source = remoteGitHub { suffix = "KhronosGroup/SPIRV-Headers"; tag = "vulkan-sdk-"+version; - checksum = "KKhVra3IQ4DcciWO7J7Vphhyw-Lb9lD1ZCjcg1AIIwsY04j1DeSIrXiVfCF67juc"; + checksum = "cngyFYwADy0Hbe96bJTunvGJh3Xj_Oa3dEfUkKpVxrBLBnpNQY_umcGMbUccP7MC"; }; exec = cmake { @@ -23,11 +23,11 @@ package spirv-tools { website = "https://github.com/KhronosGroup/SPIRV-Tools"; anitya = 334920; - version# = "1.4.357.0"; + version# = "1.4.363.0"; source = remoteGitHub { suffix = "KhronosGroup/SPIRV-Tools"; tag = "vulkan-sdk-"+version; - checksum = "yCZ92UX21pRFjVoySWiKAUermsE3A69dkeqf_8OPVn_1MxbaP5EAcKnHf27W8eYG"; + checksum = "YxfqYZtOIGgy_SuO6xadbGIUomunSxSVkShFx3qYj-_A4sGyfmbtFtqt6qYVYDeL"; }; exec = cmake { @@ -45,11 +45,11 @@ package spirv-llvm-translator { website = "https://github.com/KhronosGroup/SPIRV-LLVM-Translator"; anitya = 227273; - version# = "23.1.1"; + version# = "23.1.2"; source = remoteGitHub { suffix = "KhronosGroup/SPIRV-LLVM-Translator"; tag = "v"+version; - checksum = "8Sr087sQcaGw1yOodbRQAOv5bBj_QpxbOWGN75xxKqhRcuAUvdPZ9a009nlXZRwK"; + checksum = "6t7WCbw6xTVHF70ktDFCttrN9IO0UD4SdBiDQHtQShU7-laowtiRmXhVtRaQIhdC"; }; // litArgs emits shell syntax diff --git a/internal/rosa/package/sway/package.az b/internal/rosa/package/sway/package.az index cd7f1f7f..20834397 100644 --- a/internal/rosa/package/sway/package.az +++ b/internal/rosa/package/sway/package.az @@ -11,6 +11,7 @@ package sway { }; patches = [ "remove-unused.patch" ]; + env = [ "CFLAGS=-Wno-switch" ]; exec = meson {}; inputs = [ diff --git a/internal/rosa/package/tamago.az b/internal/rosa/package/tamago.az index d784c85a..c4393b13 100644 --- a/internal/rosa/package/tamago.az +++ b/internal/rosa/package/tamago.az @@ -3,11 +3,11 @@ package tamago { website = "https://github.com/usbarmory/tamago-go"; anitya = 388872; - version# = "1.27.1"; + version# = "1.27.2"; source = remoteGitHub { suffix = "usbarmory/tamago-go"; tag = "tamago-go"+version; - checksum = "TaWPzqqBMuhGslUm9CQvShJRjfONbFEr51mcM5zFlopYhYA6Tb58g8wRPc5a5lQ0"; + checksum = "z-4tuciBpbu4cR4hwkRemUcxWUgfLpfc8shIFDjUAdthxWMv9FYkwCfTta0Ho_4-"; }; env = [ diff --git a/internal/rosa/package/toybox.az b/internal/rosa/package/toybox.az index bd3dc966..b90e08d2 100644 --- a/internal/rosa/package/toybox.az +++ b/internal/rosa/package/toybox.az @@ -2,10 +2,10 @@ package toybox-source { description = "toybox source tree"; exclude = true; - version# = "0.8.14"; + version# = "0.8.15"; output = remoteTar { url = "https://landley.net/toybox/downloads/toybox-"+version+".tar.gz"; - checksum = "RZQp2CTsLt_y15vsZxwqUb2O1XfK7uvwn-2sTd38O4HAsFKPQpS1UP0brYJ3dRA-"; + checksum = "-0KgQ7Ogk5Nt6LLTn08b_xPFZXi4aqoBzw7XPnUwP3r2AZcfPOV10rMB0qAEzXcI"; compress = gzip; }; } diff --git a/internal/rosa/package/utf8proc.az b/internal/rosa/package/utf8proc.az index 68e904ec..a881c493 100644 --- a/internal/rosa/package/utf8proc.az +++ b/internal/rosa/package/utf8proc.az @@ -3,11 +3,11 @@ package utf8proc { website = "https://juliastrings.github.io/utf8proc"; anitya = 7455; - version# = "2.11.3"; + version# = "2.12.0"; source = remoteGitHub { suffix = "JuliaStrings/utf8proc"; tag = "v"+version; - checksum = "LlmejkymQEhaMciSE17JSl3pAYdf6DdXNr0ucwclClsMCeq7l1dcgIJl1JmNTQVq"; + checksum = "kfzU-vVmmCzyYO9CO-PGiX5Oq63f6o_Zv0G3jO4gxoyCP2qZap20Hh7qAUDMeTKo"; }; exec = cmake { diff --git a/internal/rosa/package/util-linux/package.az b/internal/rosa/package/util-linux.az index 1317e50f..8beff173 100644 --- a/internal/rosa/package/util-linux/package.az +++ b/internal/rosa/package/util-linux.az @@ -5,7 +5,7 @@ package util-linux { // release candidates confuse Anitya latest = anityaFallback; - version# = "2.42.3"; + version# = "2.42.4"; source = remoteTar { url = "https://www.kernel.org/pub/linux/utils/util-linux/"+ "v"+join { @@ -19,10 +19,9 @@ package util-linux { }; sep = "."; }+"/util-linux-"+version+".tar.gz"; - checksum = "VQ3NCM4kt0-TTrdJElBfckXNCGBv6UfXGTRm1poQ0XP0WBND4eG-O7n5Ly_a-Iw_"; + checksum = "tDzZiC8DCtH3f-QA9uI2EUKMQ4m5nPZfxVMuOsPBB2dHQS9gJ2HqVTJzRbq7p7VC"; compress = gzip; }; - patches = [ "a323dddbcd1ed05a10e7e870b3e1a48b4ed44a43.patch" ]; bin = [ "bash" ]; exec = make { diff --git a/internal/rosa/package/util-linux/a323dddbcd1ed05a10e7e870b3e1a48b4ed44a43.patch b/internal/rosa/package/util-linux/a323dddbcd1ed05a10e7e870b3e1a48b4ed44a43.patch deleted file mode 100644 index 36d6f968..00000000 --- a/internal/rosa/package/util-linux/a323dddbcd1ed05a10e7e870b3e1a48b4ed44a43.patch +++ /dev/null @@ -1,35 +0,0 @@ -From a323dddbcd1ed05a10e7e870b3e1a48b4ed44a43 Mon Sep 17 00:00:00 2001 -From: Karel Zak <kzak@redhat.com> -Date: Wed, 2 Sep 2026 13:32:27 +0200 -Subject: [PATCH] libmount: add missing fileutils.h include to hook_idmap.c - -The hook_idmap.c uses RESOLVE_NO_SYMLINKS (added by commit fb8e26535) -but does not include fileutils.h, which provides the fallback #define -for this constant. - -On Fedora (glibc 2.40+), this is masked because glibc's -<bits/fcntl-linux.h> transitively includes <linux/openat2.h>, which -defines RESOLVE_NO_SYMLINKS. On Ubuntu (and other distros with older -glibc), <fcntl.h> does not pull in openat2.h, so the build fails: - - hook_idmap.c:335:33: error: 'RESOLVE_NO_SYMLINKS' undeclared - -Fixes: fb8e26535 ("libmount: pin source path with openat2() for restricted users") -Signed-off-by: Karel Zak <kzak@redhat.com> -(cherry picked from commit 7e2e010874b10b3aabdc3c4c844c9ffc46a4a374) ---- - libmount/src/hook_idmap.c | 1 + - 1 file changed, 1 insertion(+) - -diff --git a/libmount/src/hook_idmap.c b/libmount/src/hook_idmap.c -index 77494e29810..2c697b17154 100644 ---- a/libmount/src/hook_idmap.c -+++ b/libmount/src/hook_idmap.c -@@ -23,6 +23,7 @@ - - #include "strutils.h" - #include "all-io.h" -+#include "fileutils.h" - #include "namespace.h" - - #include "mountP.h" diff --git a/internal/rosa/package/x.az b/internal/rosa/package/x.az index 73b82981..44af6d22 100644 --- a/internal/rosa/package/x.az +++ b/internal/rosa/package/x.az @@ -48,12 +48,12 @@ package xorgproto { website = "https://gitlab.freedesktop.org/xorg/proto/xorgproto"; anitya = 17190; - version# = "2025.1"; + version# = "2026.1"; source = remoteGitLab { domain = "gitlab.freedesktop.org"; suffix = "xorg/proto/xorgproto"; ref = "xorgproto-"+version; - checksum = "pTwJiBJHKA6Rgm3cVDXy1lyvXNIUzTRaukvvYdk1xWoJ_1G-Dfjm9MyewuyIjoHz"; + checksum = "-inAZyzcTjFH1wfc2Zw_mdfx9K1YG0ieQ8kF5u3ZS1QykAavZ0T238EjljRzEYZ-"; }; exec = make { @@ -242,12 +242,12 @@ package libX11 { website = "https://gitlab.freedesktop.org/xorg/lib/libx11"; anitya = 1764; - version# = "1.8.13"; + version# = "1.8.14"; source = remoteGitLab { domain = "gitlab.freedesktop.org"; suffix = "xorg/lib/libx11"; ref = "libX11-"+version; - checksum = "ARh-cuZY_U2v3DbPS1byc7ybh9NInZc-yav7SJiusk_C7408s058qWV83ocMd2pT"; + checksum = "9huww9V6fAjKhiG7cWpV-zIQ8aTt9JiuO7qf0yuiLvU4B_5d6XA2y2GvOBQ06XAZ"; }; exec = make { @@ -337,12 +337,12 @@ package libXrender { website = "https://gitlab.freedesktop.org/xorg/lib/libxrender"; anitya = 1789; - version# = "0.9.12"; + version# = "0.9.13"; source = remoteGitLab { domain = "gitlab.freedesktop.org"; suffix = "xorg/lib/libxrender"; ref = "libXrender-"+version; - checksum = "s6nWn-r17X_W-7anaS7wIcq5_Levf9zAigwJDD9Klp-cCJTVDhLo7GufuVOrxVP0"; + checksum = "RGzBPut1QMqP5nY2cn0tybZAvakvaOXPM_DPRgNox9PT_8ZLycfxHQZzJhilzCI5"; }; exec = make { @@ -676,12 +676,12 @@ package xserver { website = "https://gitlab.freedesktop.org/xorg/xserver"; anitya = 5250; - version# = "26.0.99.901"; + version# = "26.0.99.903"; source = remoteGitLab { domain = "gitlab.freedesktop.org"; suffix = "xorg/xserver"; ref = "xorg-server-"+version; - checksum = "2Liwqj-tu_NuD8BuWZR0PyYVJ_A0nj_sgx-LZWtdVpKAKQHtN-dSWCl-IPVOlcHG"; + checksum = "YVT4TT4pI-N8cEPLty3MuJsy6w7K-EqrIJ9jp8c6AzV4PjN8ZYr6HdeHO5MnTsjF"; }; exec = meson { @@ -753,12 +753,12 @@ package xwayland { website = "https://gitlab.freedesktop.org/xorg/xserver"; anitya = 180949; - version# = "26.0.99.901"; + version# = "26.0.99.903"; source = remoteGitLab { domain = "gitlab.freedesktop.org"; suffix = "xorg/xserver"; ref = "xwayland-"+version; - checksum = "Vgtnwme2HU3CCeLE_xNScV_uJ55kPMremaOyVWRUFaRpwpaOQxsNB2fde_9Rx5zm"; + checksum = "TGto_v7iLObFrQ6tsS9OHR2sJlpeYp1akqiwVnn51dZ73kF_sbWZ8Zm0HiGwIfPV"; }; bin = [ "bash" ]; diff --git a/internal/rosa/package/xz.az b/internal/rosa/package/xz.az index 0a186c28..3bb91b5f 100644 --- a/internal/rosa/package/xz.az +++ b/internal/rosa/package/xz.az @@ -3,12 +3,12 @@ package xz { website = "https://tukaani.org/xz"; anitya = 5277; - version# = "5.8.3"; + version# = "5.8.4"; source = remoteGitHubRelease { suffix = "tukaani-project/xz"; tag = "v"+version; name = "xz-"+version+".tar.bz2"; - checksum = "nCdayphPGdIdVoAZ2hR4vYlhDG9LeVKho_i7ealTud4Vxy5o5dWe0VwFlN7utuUL"; + checksum = "sYLUjO5X0QREmCBYceLU3CSj-HUCd3g0kJAHq8F2GO0Y6-LcyKYbESyRq4sr7Bih"; compress = bzip2; }; diff --git a/internal/rosa/state.go b/internal/rosa/state.go index f2ba2059..01abf736 100644 --- a/internal/rosa/state.go +++ b/internal/rosa/state.go @@ -1339,6 +1339,30 @@ func (s *S) RegisterFS(fsys fs.FS) error { return nil } +// setSource overrides the hakurei-source package with the specified tarball and +// version string. The resulting IR is curable on the daemon. Must not be used +// concurrently with any other method. +func (s *S) setSource(p []byte, version string) { + const name = "hakurei-source" + s.p.Store( + H(name), + Artifact(func(t Toolchain) (*Metadata, pkg.Artifact) { + return &Metadata{ + Name: name, + Description: "hakurei source tree (current)", + Version: version, + Exclude: true, + }, pkg.NewTar( + pkg.NewDecompress( + pkg.NewFile("hakurei-src-current.tar.gz", p), + pkg.Gzip, + ), + ) + }), + ) + s.DropCaches(s.Arch(), s.Flags()) +} + // SetSource overrides the hakurei-source package with a cached tarball of fsys. // The resulting IR is curable on the daemon. Must not be used concurrently with // any other method. @@ -1416,20 +1440,7 @@ func (s *S) SetSource(fsys fs.FS) error { return err } - const name = "hakurei-source" - a := pkg.NewFile("hakurei-src-current.tar.gz", buf.Bytes()) - s.p.Store( - H(name), - Artifact(func(t Toolchain) (*Metadata, pkg.Artifact) { - return &Metadata{ - Name: name, - Description: "hakurei source tree (current)", - Version: version, - Exclude: true, - }, pkg.NewTar(pkg.NewDecompress(a, pkg.Gzip)) - }), - ) - s.DropCaches(s.Arch(), s.Flags()) + s.setSource(buf.Bytes(), version) return nil } diff --git a/internal/system/acl.go b/internal/system/acl.go index e91f4eb7..7d34fa62 100644 --- a/internal/system/acl.go +++ b/internal/system/acl.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "os" - "slices" "hakurei.app/check" "hakurei.app/hst" @@ -12,14 +11,14 @@ import ( ) // UpdatePerm calls UpdatePermType with the [Process] criteria. -func (sys *I) UpdatePerm(path *check.Absolute, perms ...acl.Perm) *I { - sys.UpdatePermType(Process, path, perms...) +func (sys *I) UpdatePerm(path *check.Absolute, perms acl.Perm) *I { + sys.UpdatePermType(Process, path, perms) return sys } // UpdatePermType maintains [acl.Perms] on a file until its [hst.Enablements] is // no longer satisfied. -func (sys *I) UpdatePermType(et hst.Enablements, path *check.Absolute, perms ...acl.Perm) *I { +func (sys *I) UpdatePermType(et hst.Enablements, path *check.Absolute, perms acl.Perm) *I { sys.ops = append(sys.ops, &aclUpdateOp{et, path.String(), perms}) return sys } @@ -28,20 +27,20 @@ func (sys *I) UpdatePermType(et hst.Enablements, path *check.Absolute, perms ... type aclUpdateOp struct { et hst.Enablements path string - perms acl.Perms + perms acl.Perm } func (a *aclUpdateOp) Type() hst.Enablements { return a.et } func (a *aclUpdateOp) apply(sys *I) error { sys.msg.Verbose("applying ACL", a) - return newOpError("acl", sys.aclUpdate(a.path, sys.uid, a.perms...), false) + return newOpError("acl", sys.aclUpdate(a.path, sys.uid, a.perms), false) } func (a *aclUpdateOp) revert(sys *I, ec *Criteria) error { if ec.hasType(a.Type()) { sys.msg.Verbose("stripping ACL", a) - err := sys.aclUpdate(a.path, sys.uid) + err := sys.aclUpdate(a.path, sys.uid, 0) if errors.Is(err, os.ErrNotExist) { // the ACL is effectively stripped if the file no longer exists sys.msg.Verbosef("target of ACL %s no longer exists", a) @@ -57,9 +56,7 @@ func (a *aclUpdateOp) revert(sys *I, ec *Criteria) error { func (a *aclUpdateOp) Is(o Op) bool { target, ok := o.(*aclUpdateOp) return ok && a != nil && target != nil && - a.et == target.et && - a.path == target.path && - slices.Equal(a.perms, target.perms) + *a == *target } func (a *aclUpdateOp) Path() string { return a.path } diff --git a/internal/system/acl_test.go b/internal/system/acl_test.go index 01f9d9b1..a340e234 100644 --- a/internal/system/acl_test.go +++ b/internal/system/acl_test.go @@ -15,45 +15,45 @@ func TestACLUpdateOp(t *testing.T) { checkOpBehaviour(t, 0, []opBehaviourTestCase{ {"apply aclUpdate", 0xbeef, 0xff, - &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, stub.UniqueError(1)), + &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}, []stub.Call{ + call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}}}, nil, nil), + call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, acl.Read | acl.Write | acl.Execute}, nil, stub.UniqueError(1)), }, &OpError{Op: "acl", Err: stub.UniqueError(1)}, nil, nil}, {"revert aclUpdate", 0xbeef, 0xff, - &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil), + &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}, []stub.Call{ + call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}}}, nil, nil), + call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, acl.Read | acl.Write | acl.Execute}, nil, nil), }, nil, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, ([]acl.Perm)(nil)}, nil, stub.UniqueError(0)), + call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}}}, nil, nil), + call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, acl.Perm(0)}, nil, stub.UniqueError(0)), }, &OpError{Op: "acl", Err: stub.UniqueError(0), Revert: true}}, {"success revert skip", 0xbeef, Process, - &aclUpdateOp{User, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{User, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil), + &aclUpdateOp{User, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}, []stub.Call{ + call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{User, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}}}, nil, nil), + call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, acl.Read | acl.Write | acl.Execute}, nil, nil), }, nil, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"skipping ACL", &aclUpdateOp{User, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil), + call("verbose", stub.ExpectArgs{[]any{"skipping ACL", &aclUpdateOp{User, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}}}, nil, nil), }, nil}, {"success revert aclUpdate ENOENT", 0xbeef, 0xff, - &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil), + &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}, []stub.Call{ + call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}}}, nil, nil), + call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, acl.Read | acl.Write | acl.Execute}, nil, nil), }, nil, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, ([]acl.Perm)(nil)}, nil, &os.PathError{Op: "acl_get_file", Path: "/proc/nonexistent", Err: syscall.ENOENT}), - call("verbosef", stub.ExpectArgs{"target of ACL %s no longer exists", []any{&aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil), + call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}}}, nil, nil), + call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, acl.Perm(0)}, nil, &os.PathError{Op: "acl_get_file", Path: "/proc/nonexistent", Err: syscall.ENOENT}), + call("verbosef", stub.ExpectArgs{"target of ACL %s no longer exists", []any{&aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}}}, nil, nil), }, nil}, {"success", 0xbeef, 0xff, - &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil), + &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}, []stub.Call{ + call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}}}, nil, nil), + call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, acl.Read | acl.Write | acl.Execute}, nil, nil), }, nil, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, ([]acl.Perm)(nil)}, nil, nil), + call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", acl.Read | acl.Write | acl.Execute}}}, nil, nil), + call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, acl.Perm(0)}, nil, nil), }, nil}, }) @@ -63,28 +63,28 @@ func TestACLUpdateOp(t *testing.T) { func(_ *testing.T, sys *I) { sys. UpdatePerm(m("/run/user/1971/hakurei"), acl.Execute). - UpdatePerm(m("/tmp/hakurei.0/tmpdir/150"), acl.Read, acl.Write, acl.Execute) + UpdatePerm(m("/tmp/hakurei.0/tmpdir/150"), acl.Read|acl.Write|acl.Execute) }, []Op{ - &aclUpdateOp{Process, "/run/user/1971/hakurei", []acl.Perm{acl.Execute}}, - &aclUpdateOp{Process, "/tmp/hakurei.0/tmpdir/150", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, + &aclUpdateOp{Process, "/run/user/1971/hakurei", acl.Execute}, + &aclUpdateOp{Process, "/tmp/hakurei.0/tmpdir/150", acl.Read | acl.Write | acl.Execute}, }, stub.Expect{}}, {"tmpdirp", 0xbeef, func(_ *testing.T, sys *I) { sys.UpdatePermType(User, m("/tmp/hakurei.0/tmpdir"), acl.Execute) }, []Op{ - &aclUpdateOp{User, "/tmp/hakurei.0/tmpdir", []acl.Perm{acl.Execute}}, + &aclUpdateOp{User, "/tmp/hakurei.0/tmpdir", acl.Execute}, }, stub.Expect{}}, {"tmpdir", 0xbeef, func(_ *testing.T, sys *I) { - sys.UpdatePermType(User, m("/tmp/hakurei.0/tmpdir/150"), acl.Read, acl.Write, acl.Execute) + sys.UpdatePermType(User, m("/tmp/hakurei.0/tmpdir/150"), acl.Read|acl.Write|acl.Execute) }, []Op{ - &aclUpdateOp{User, "/tmp/hakurei.0/tmpdir/150", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, + &aclUpdateOp{User, "/tmp/hakurei.0/tmpdir/150", acl.Read | acl.Write | acl.Execute}, }, stub.Expect{}}, {"share", 0xbeef, func(_ *testing.T, sys *I) { sys.UpdatePermType(Process, m("/run/user/1971/hakurei/fcb8a12f7c482d183ade8288c3de78b5"), acl.Execute) }, []Op{ - &aclUpdateOp{Process, "/run/user/1971/hakurei/fcb8a12f7c482d183ade8288c3de78b5", []acl.Perm{acl.Execute}}, + &aclUpdateOp{Process, "/run/user/1971/hakurei/fcb8a12f7c482d183ade8288c3de78b5", acl.Execute}, }, stub.Expect{}}, {"passwd", 0xbeef, func(_ *testing.T, sys *I) { @@ -92,14 +92,14 @@ func TestACLUpdateOp(t *testing.T) { UpdatePermType(Process, m("/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/passwd"), acl.Read). UpdatePermType(Process, m("/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/group"), acl.Read) }, []Op{ - &aclUpdateOp{Process, "/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/passwd", []acl.Perm{acl.Read}}, - &aclUpdateOp{Process, "/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/group", []acl.Perm{acl.Read}}, + &aclUpdateOp{Process, "/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/passwd", acl.Read}, + &aclUpdateOp{Process, "/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/group", acl.Read}, }, stub.Expect{}}, {"wayland", 0xbeef, func(_ *testing.T, sys *I) { - sys.UpdatePermType(hst.EWayland, m("/run/user/1971/wayland-0"), acl.Read, acl.Write, acl.Execute) + sys.UpdatePermType(hst.EWayland, m("/run/user/1971/wayland-0"), acl.Read|acl.Write|acl.Execute) }, []Op{ - &aclUpdateOp{hst.EWayland, "/run/user/1971/wayland-0", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, + &aclUpdateOp{hst.EWayland, "/run/user/1971/wayland-0", acl.Read | acl.Write | acl.Execute}, }, stub.Expect{}}, }) @@ -110,75 +110,75 @@ func TestACLUpdateOp(t *testing.T) { {"et differs", &aclUpdateOp{ hst.EWayland, "/run/user/1971/wayland-0", - []acl.Perm{acl.Read, acl.Write, acl.Execute}, + acl.Read | acl.Write | acl.Execute, }, &aclUpdateOp{ hst.EX11, "/run/user/1971/wayland-0", - []acl.Perm{acl.Read, acl.Write, acl.Execute}, + acl.Read | acl.Write | acl.Execute, }, false}, {"path differs", &aclUpdateOp{ hst.EWayland, "/run/user/1971/wayland-0", - []acl.Perm{acl.Read, acl.Write, acl.Execute}, + acl.Read | acl.Write | acl.Execute, }, &aclUpdateOp{ hst.EWayland, "/run/user/1971/wayland-1", - []acl.Perm{acl.Read, acl.Write, acl.Execute}, + acl.Read | acl.Write | acl.Execute, }, false}, {"perms differs", &aclUpdateOp{ hst.EWayland, "/run/user/1971/wayland-0", - []acl.Perm{acl.Read, acl.Write, acl.Execute}, + acl.Read | acl.Write | acl.Execute, }, &aclUpdateOp{ hst.EWayland, "/run/user/1971/wayland-0", - []acl.Perm{acl.Read, acl.Write}, + acl.Read | acl.Write, }, false}, {"equals", &aclUpdateOp{ hst.EWayland, "/run/user/1971/wayland-0", - []acl.Perm{acl.Read, acl.Write, acl.Execute}, + acl.Read | acl.Write | acl.Execute, }, &aclUpdateOp{ hst.EWayland, "/run/user/1971/wayland-0", - []acl.Perm{acl.Read, acl.Write, acl.Execute}, + acl.Read | acl.Write | acl.Execute, }, true}, }) checkOpMeta(t, []opMetaTestCase{ {"clear", - &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{}}, + &aclUpdateOp{Process, "/proc/nonexistent", 0}, Process, "/proc/nonexistent", `--- type: process path: "/proc/nonexistent"`}, {"read", - &aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/0", []acl.Perm{acl.Read}}, + &aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/0", acl.Read}, User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/0", `r-- type: user path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/0"`}, {"write", - &aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/1", []acl.Perm{acl.Write}}, + &aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/1", acl.Write}, User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/1", `-w- type: user path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/1"`}, {"execute", - &aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/2", []acl.Perm{acl.Execute}}, + &aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/2", acl.Execute}, User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/2", `--x type: user path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/2"`}, {"wayland", - &aclUpdateOp{hst.EWayland, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland", []acl.Perm{acl.Read, acl.Write}}, + &aclUpdateOp{hst.EWayland, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland", acl.Read | acl.Write}, hst.EWayland, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland", `rw- type: wayland path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland"`}, {"x11", - &aclUpdateOp{hst.EX11, "/tmp/.X11-unix/X0", []acl.Perm{acl.Read, acl.Execute}}, + &aclUpdateOp{hst.EX11, "/tmp/.X11-unix/X0", acl.Read | acl.Execute}, hst.EX11, "/tmp/.X11-unix/X0", `r-x type: x11 path: "/tmp/.X11-unix/X0"`}, {"dbus", - &aclUpdateOp{hst.EDBus, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus", []acl.Perm{acl.Write, acl.Execute}}, + &aclUpdateOp{hst.EDBus, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus", acl.Write | acl.Execute}, hst.EDBus, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus", `-wx type: dbus path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus"`}, {"pulseaudio", - &aclUpdateOp{hst.EPulse, "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, + &aclUpdateOp{hst.EPulse, "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse", acl.Read | acl.Write | acl.Execute}, hst.EPulse, "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse", `rwx type: pulseaudio path: "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse"`}, }) diff --git a/internal/system/dispatcher.go b/internal/system/dispatcher.go index 8f70b0c0..5a222176 100644 --- a/internal/system/dispatcher.go +++ b/internal/system/dispatcher.go @@ -50,7 +50,7 @@ type syscallDispatcher interface { println(v ...any) // aclUpdate provides [acl.Update]. - aclUpdate(name string, uid int, perms ...acl.Perm) error + aclUpdate(name string, uid int, perms acl.Perm) error // waylandNew provides [wayland.New]. waylandNew(displayPath, bindPath *check.Absolute, appID, instanceID string) (io.Closer, error) @@ -85,8 +85,8 @@ func (k direct) remove(name string) error { return os.Remove(na func (k direct) println(v ...any) { log.Println(v...) } -func (k direct) aclUpdate(name string, uid int, perms ...acl.Perm) error { - return acl.Update(name, uid, perms...) +func (k direct) aclUpdate(name string, uid int, perms acl.Perm) error { + return acl.Update(name, uid, perms) } func (k direct) waylandNew(displayPath, bindPath *check.Absolute, appID, instanceID string) (io.Closer, error) { diff --git a/internal/system/dispatcher_test.go b/internal/system/dispatcher_test.go index 3bf14e75..bfb8df72 100644 --- a/internal/system/dispatcher_test.go +++ b/internal/system/dispatcher_test.go @@ -2,11 +2,13 @@ package system import ( "io" + "io/fs" "log" "os" "reflect" "slices" "testing" + "time" "unsafe" "hakurei.app/check" @@ -206,7 +208,7 @@ func checkOpMeta(t *testing.T, testCases []opMetaTestCase) { // InternalNew initialises [I] with a stub syscallDispatcher. func InternalNew(t *testing.T, want stub.Expect, uid int) (*I, *stub.Stub[syscallDispatcher]) { k := &kstub{stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{s} }, want)} - sys := New(t.Context(), k, uid) + sys := New(t.Context(), k, uid, nil) sys.syscallDispatcher = k return sys, k.Stub } @@ -215,6 +217,17 @@ type kstub struct{ *stub.Stub[syscallDispatcher] } func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) } +// modeFi implements the Mode method of [os.FileInfo]. +type modeFi os.FileMode + +func (modeFi) Name() string { panic("attempting to call Name") } +func (modeFi) Size() int64 { panic("attempting to call Size") } +func (modeFi) ModTime() time.Time { panic("attempting to call ModTime") } +func (modeFi) Sys() any { panic("attempting to call Sys") } + +func (fi modeFi) Mode() fs.FileMode { return fs.FileMode(fi) } +func (fi modeFi) IsDir() bool { return fs.FileMode(fi).IsDir() } + func (k *kstub) stat(name string) (fi os.FileInfo, err error) { k.Helper() expect := k.Expects("stat") @@ -280,7 +293,7 @@ func (k *kstub) println(v ...any) { } } -func (k *kstub) aclUpdate(name string, uid int, perms ...acl.Perm) error { +func (k *kstub) aclUpdate(name string, uid int, perms acl.Perm) error { k.Helper() expect := k.Expects("aclUpdate") @@ -293,7 +306,7 @@ func (k *kstub) aclUpdate(name string, uid int, perms ...acl.Perm) error { return expect.Error( stub.CheckArgReflect(k.Stub, "name", nameVal, 0), stub.CheckArg(k.Stub, "uid", uid, 1), - stub.CheckArgReflect(k.Stub, "perms", perms, 2)) + stub.CheckArg(k.Stub, "perms", perms, 2)) } func (k *kstub) waylandNew(displayPath, bindPath *check.Absolute, appID, instanceID string) (io.Closer, error) { diff --git a/internal/system/mkdir.go b/internal/system/mkdir.go index 81b89d58..ecbfc876 100644 --- a/internal/system/mkdir.go +++ b/internal/system/mkdir.go @@ -4,21 +4,33 @@ import ( "errors" "fmt" "os" + "syscall" "hakurei.app/check" "hakurei.app/hst" ) // Ensure ensures the existence of a directory. -func (sys *I) Ensure(name *check.Absolute, perm os.FileMode) *I { - sys.ops = append(sys.ops, &mkdirOp{User, name.String(), perm, false}) +func (sys *I) Ensure(name *check.Absolute, perm os.FileMode, skipChmod bool) *I { + sys.ops = append(sys.ops, &mkdirOp{ + et: User, + path: name.String(), + perm: perm, + skipChmod: skipChmod, + }) return sys } // Ephemeral ensures the existence of a directory until its [hst.Enablements] is // no longer satisfied. -func (sys *I) Ephemeral(et hst.Enablements, name *check.Absolute, perm os.FileMode) *I { - sys.ops = append(sys.ops, &mkdirOp{et, name.String(), perm, true}) +func (sys *I) Ephemeral(et hst.Enablements, name *check.Absolute, perm os.FileMode, skipChmod bool) *I { + sys.ops = append(sys.ops, &mkdirOp{ + et: et, + path: name.String(), + perm: perm, + ephemeral: true, + skipChmod: skipChmod, + }) return sys } @@ -28,6 +40,7 @@ type mkdirOp struct { path string perm os.FileMode ephemeral bool + skipChmod bool } func (m *mkdirOp) Type() hst.Enablements { return m.et } @@ -35,12 +48,21 @@ func (m *mkdirOp) Type() hst.Enablements { return m.et } func (m *mkdirOp) apply(sys *I) error { sys.msg.Verbose("ensuring directory", m) - if err := sys.mkdir(m.path, m.perm); err != nil { - if !errors.Is(err, os.ErrExist) { - return newOpError("mkdir", err, false) + if fi, err := sys.stat(m.path); err == nil { + if !fi.IsDir() { + return newOpError("mkdir", syscall.ENOTDIR, false) + } + if !m.skipChmod && fi.Mode().Perm() != m.perm { + // directory exists, ensure mode + return newOpError("mkdir", sys.chmod(m.path, m.perm), false) } - // directory exists, ensure mode - return newOpError("mkdir", sys.chmod(m.path, m.perm), false) + return nil + } else if !errors.Is(err, os.ErrNotExist) { + return newOpError("mkdir", err, false) + } + + if err := sys.mkdir(m.path, m.perm); err != nil { + return newOpError("mkdir", err, false) } else { return nil } diff --git a/internal/system/mkdir_test.go b/internal/system/mkdir_test.go index f226a48d..ea79d5b0 100644 --- a/internal/system/mkdir_test.go +++ b/internal/system/mkdir_test.go @@ -2,6 +2,7 @@ package system import ( "os" + "syscall" "testing" "hakurei.app/internal/stub" @@ -11,63 +12,78 @@ func TestMkdirOp(t *testing.T) { t.Parallel() checkOpBehaviour(t, 0, []opBehaviourTestCase{ - {"mkdir", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}}}, nil, nil), + {"mkdir", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}, []stub.Call{ + call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}}}, nil, nil), + call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, os.ErrNotExist), call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, stub.UniqueError(2)), }, &OpError{Op: "mkdir", Err: stub.UniqueError(2)}, nil, nil}, - {"chmod", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}}}, nil, nil), - call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, os.ErrExist), + {"not directory", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}, []stub.Call{ + call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}}}, nil, nil), + call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, modeFi(0700), nil), + }, &OpError{Op: "mkdir", Err: syscall.ENOTDIR}, nil, nil}, + + {"chmod", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}, []stub.Call{ + call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}}}, nil, nil), + call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, modeFi(os.ModeDir|0700), nil), call("chmod", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, stub.UniqueError(1)), }, &OpError{Op: "mkdir", Err: stub.UniqueError(1)}, nil, nil}, - {"remove", 0xbeef, 0xff, &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), + {"remove", 0xbeef, 0xff, &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}, []stub.Call{ + call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil), + call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, os.ErrNotExist), call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil), }, nil, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), + call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil), call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, stub.UniqueError(0)), }, &OpError{Op: "mkdir", Err: stub.UniqueError(0), Revert: true}}, - {"success exist chmod", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}}}, nil, nil), - call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, os.ErrExist), + {"success exist chmod", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}, []stub.Call{ + call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}}}, nil, nil), + call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, modeFi(os.ModeDir|0700), nil), call("chmod", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil), }, nil, nil, nil}, - {"success ensure", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}}}, nil, nil), + {"success exist", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}, []stub.Call{ + call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}}}, nil, nil), + call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, modeFi(os.ModeDir|0711), nil), + }, nil, nil, nil}, + + {"success ensure", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}, []stub.Call{ + call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false, false}}}, nil, nil), + call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, os.ErrNotExist), call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil), }, nil, nil, nil}, - {"success skip", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), + {"success skip", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}, []stub.Call{ + call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil), + call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, os.ErrNotExist), call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil), }, nil, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"skipping ephemeral directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), + call("verbose", stub.ExpectArgs{[]any{"skipping ephemeral directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil), }, nil}, - {"success", 0xbeef, 0xff, &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), + {"success", 0xbeef, 0xff, &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}, []stub.Call{ + call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil), + call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, os.ErrNotExist), call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil), }, nil, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), + call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil), call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, nil), }, nil}, }) checkOpsBuilder(t, "EnsureEphemeral", []opsBuilderTestCase{ {"ensure", 0xcafe, func(_ *testing.T, sys *I) { - sys.Ensure(m("/tmp/hakurei.0"), 0700) + sys.Ensure(m("/tmp/hakurei.0"), 0700, false) }, []Op{ - &mkdirOp{User, "/tmp/hakurei.0", 0700, false}, + &mkdirOp{User, "/tmp/hakurei.0", 0700, false, false}, }, stub.Expect{}}, {"ephemeral", 0xcafe, func(_ *testing.T, sys *I) { - sys.Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711) + sys.Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711, false) }, []Op{ - &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}, + &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}, }, stub.Expect{}}, }) @@ -76,37 +92,37 @@ func TestMkdirOp(t *testing.T) { {"zero", new(mkdirOp), new(mkdirOp), true}, {"ephemeral differs", - &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, false}, - &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true}, + &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, false, false}, + &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true, false}, false}, {"perm differs", - &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0701, true}, - &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true}, + &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0701, true, false}, + &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true, false}, false}, {"path differs", - &mkdirOp{Process, "/tmp/hakurei.1/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true}, - &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true}, + &mkdirOp{Process, "/tmp/hakurei.1/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true, false}, + &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true, false}, false}, {"et differs", - &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true}, - &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true}, + &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true, false}, + &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true, false}, false}, {"equals", - &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true}, - &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true}, + &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true, false}, + &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true, false}, true}, }) checkOpMeta(t, []opMetaTestCase{ - {"ensure", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, false}, + {"ensure", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, false, false}, User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", `mode: -rwx------ type: ensure path: "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"`}, - {"ephemeral", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true}, + {"ephemeral", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true, false}, User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", `mode: -rwx------ type: user path: "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"`}, }) diff --git a/internal/system/pipewire.go b/internal/system/pipewire.go index c2d7b396..427013a4 100644 --- a/internal/system/pipewire.go +++ b/internal/system/pipewire.go @@ -73,7 +73,7 @@ func (p *pipewireOp) apply(sys *I) (err error) { return newOpError("pipewire", err, false) } - if err = sys.aclUpdate(p.dst.String(), sys.uid, acl.Read, acl.Write, acl.Execute); err != nil { + if err = sys.aclUpdate(p.dst.String(), sys.uid, acl.Read|acl.Write|acl.Execute); err != nil { if closeErr := p.scc.Close(); closeErr != nil { return newOpError("pipewire", errors.Join(err, closeErr), false) } diff --git a/internal/system/pipewire_test.go b/internal/system/pipewire_test.go index f0ca0b34..5953681e 100644 --- a/internal/system/pipewire_test.go +++ b/internal/system/pipewire_test.go @@ -423,7 +423,7 @@ func TestPipeWireOp(t *testing.T) { call("verbosef", stub.ExpectArgs{"pipewire pathname socket on %q", []any{ignoreValue{}}}, nil, nil), call("chmod", stub.ExpectArgs{ignoreValue{}, os.FileMode(0)}, nil, nil), - call("aclUpdate", stub.ExpectArgs{ignoreValue{}, 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil), + call("aclUpdate", stub.ExpectArgs{ignoreValue{}, 0xbeef, acl.Read | acl.Write | acl.Execute}, nil, nil), }, nil, []stub.Call{ call("verbosef", stub.ExpectArgs{"hanging up pipewire socket on %q", []any{ignoreValue{}}}, nil, nil), }, nil}, diff --git a/internal/system/system.go b/internal/system/system.go index 1462f818..340e23ec 100644 --- a/internal/system/system.go +++ b/internal/system/system.go @@ -6,7 +6,9 @@ import ( "errors" "strings" + "hakurei.app/check" "hakurei.app/hst" + "hakurei.app/internal/lockedfile" "hakurei.app/message" ) @@ -67,14 +69,6 @@ func TypeString(e hst.Enablements) string { } } -// New returns the address of a new [I] targeting uid. -func New(ctx context.Context, msg message.Msg, uid int) (sys *I) { - if ctx == nil || msg == nil || uid < 0 { - panic("invalid call to New") - } - return &I{ctx: ctx, msg: msg, uid: uid, syscallDispatcher: direct{}} -} - // An I provides deferred operating system interaction. [I] must not be copied. // Methods of [I] must not be used concurrently. type I struct { @@ -84,15 +78,32 @@ type I struct { ops []Op ctx context.Context - // the behaviour of Commit is only defined for up to one call - committed bool - // the behaviour of Revert is only defined for up to one call - reverted bool + // The behaviour of Commit and Revert is only defined for at most one call. + committed, reverted bool + // Optional Commit and Revert big lock. + mu *lockedfile.Mutex msg message.Msg syscallDispatcher } +// New returns the address of a new [I] targeting uid. +func New( + ctx context.Context, + msg message.Msg, + uid int, + lock *check.Absolute, +) (sys *I) { + if ctx == nil || msg == nil || uid < 0 { + panic("invalid call to New") + } + v := I{ctx: ctx, msg: msg, uid: uid, syscallDispatcher: direct{}} + if lock != nil { + v.mu = lockedfile.MutexAt(lock.String()) + } + return &v +} + func (sys *I) UID() int { return sys.uid } // Equal returns whether all [Op] instances held by sys matches that of target. @@ -118,9 +129,18 @@ func (sys *I) Commit() error { if sys.committed { panic("attempting to commit twice") } + + if sys.mu != nil { + if unlock, err := sys.mu.Lock(); err != nil { + return err + } else { + defer unlock() + } + } sys.committed = true - sp := New(sys.ctx, sys.msg, sys.uid) + sp := New(sys.ctx, sys.msg, sys.uid, nil) + // lock is still held on revert sp.syscallDispatcher = sys.syscallDispatcher sp.ops = make([]Op, 0, len(sys.ops)) // prevent copies during commits defer func() { @@ -153,6 +173,13 @@ func (sys *I) Revert(ec *Criteria) error { if sys.reverted { panic("attempting to revert twice") } + if sys.mu != nil { + if unlock, err := sys.mu.Lock(); err != nil { + return err + } else { + defer unlock() + } + } sys.reverted = true // collect errors diff --git a/internal/system/system_test.go b/internal/system/system_test.go index b5dec992..8d863826 100644 --- a/internal/system/system_test.go +++ b/internal/system/system_test.go @@ -82,7 +82,7 @@ func TestNew(t *testing.T) { t.Errorf("recover: %v, want %v", r, want) } }() - New(nil, message.New(nil), 0) + New(nil, message.New(nil), 0, nil) }) t.Run("msg", func(t *testing.T) { @@ -92,7 +92,7 @@ func TestNew(t *testing.T) { t.Errorf("recover: %v, want %v", r, want) } }() - New(t.Context(), nil, 0) + New(t.Context(), nil, 0, nil) }) t.Run("uid", func(t *testing.T) { @@ -102,11 +102,11 @@ func TestNew(t *testing.T) { t.Errorf("recover: %v, want %v", r, want) } }() - New(t.Context(), message.New(nil), -1) + New(t.Context(), message.New(nil), -1, nil) }) }) - sys := New(t.Context(), message.New(nil), 0xbeef) + sys := New(t.Context(), message.New(nil), 0xbeef, nil) if sys.ctx == nil { t.Error("New: ctx = nil") } @@ -125,53 +125,53 @@ func TestEqual(t *testing.T) { want bool }{ {"simple UID", - New(t.Context(), message.New(nil), 150), - New(t.Context(), message.New(nil), 150), + New(t.Context(), message.New(nil), 150, nil), + New(t.Context(), message.New(nil), 150, nil), true}, {"simple UID differ", - New(t.Context(), message.New(nil), 150), - New(t.Context(), message.New(nil), 151), + New(t.Context(), message.New(nil), 150, nil), + New(t.Context(), message.New(nil), 151, nil), false}, {"simple UID nil", - New(t.Context(), message.New(nil), 150), + New(t.Context(), message.New(nil), 150, nil), nil, false}, {"op length mismatch", - New(t.Context(), message.New(nil), 150). + New(t.Context(), message.New(nil), 150, nil). ChangeHosts("chronos"), - New(t.Context(), message.New(nil), 150). + New(t.Context(), message.New(nil), 150, nil). ChangeHosts("chronos"). - Ensure(m("/run"), 0755), + Ensure(m("/run"), 0755, false), false}, {"op value mismatch", - New(t.Context(), message.New(nil), 150). + New(t.Context(), message.New(nil), 150, nil). ChangeHosts("chronos"). - Ensure(m("/run"), 0644), - New(t.Context(), message.New(nil), 150). + Ensure(m("/run"), 0644, false), + New(t.Context(), message.New(nil), 150, nil). ChangeHosts("chronos"). - Ensure(m("/run"), 0755), + Ensure(m("/run"), 0755, false), false}, {"op type mismatch", - New(t.Context(), message.New(nil), 150). + New(t.Context(), message.New(nil), 150, nil). ChangeHosts("chronos"). Wayland(m("/proc/nonexistent/dst"), m("/proc/nonexistent/src"), "\x00", "\x00"), - New(t.Context(), message.New(nil), 150). + New(t.Context(), message.New(nil), 150, nil). ChangeHosts("chronos"). - Ensure(m("/run"), 0755), + Ensure(m("/run"), 0755, false), false}, {"op equals", - New(t.Context(), message.New(nil), 150). + New(t.Context(), message.New(nil), 150, nil). ChangeHosts("chronos"). - Ensure(m("/run"), 0755), - New(t.Context(), message.New(nil), 150). + Ensure(m("/run"), 0755, false), + New(t.Context(), message.New(nil), 150, nil). ChangeHosts("chronos"). - Ensure(m("/run"), 0755), + Ensure(m("/run"), 0755, false), true}, } @@ -200,46 +200,49 @@ func TestCommitRevert(t *testing.T) { }{ {"apply xhost partial mkdir", func(sys *I) { sys. - Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711). + Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711, false). ChangeHosts("chronos") }, 0xff, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), + call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil), + call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, os.ErrNotExist), call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil), call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil), call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, stub.UniqueError(2)), call("verbosef", stub.ExpectArgs{"commit faulted after %d ops, rolling back partial commit", []any{1}}, nil, nil), - call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), + call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil), call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, stub.UniqueError(3)), call("println", stub.ExpectArgs{[]any{"cannot revert mkdir: unique error 3 injected by the test suite"}}, nil, nil), }, &OpError{Op: "xhost", Err: stub.UniqueError(2)}, nil, nil}, {"apply xhost", func(sys *I) { sys. - Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711). + Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711, false). ChangeHosts("chronos") }, 0xff, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), + call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil), + call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, os.ErrNotExist), call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil), call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil), call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, stub.UniqueError(2)), call("verbosef", stub.ExpectArgs{"commit faulted after %d ops, rolling back partial commit", []any{1}}, nil, nil), - call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), + call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil), call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, nil), }, &OpError{Op: "xhost", Err: stub.UniqueError(2)}, nil, nil}, {"revert multi", func(sys *I) { sys. - Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711). + Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711, false). ChangeHosts("chronos") }, 0xff, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), + call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil), + call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, os.ErrNotExist), call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil), call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil), call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil), }, nil, []stub.Call{ call("verbosef", stub.ExpectArgs{"deleting entry %s from X11", []any{xhostOp("chronos")}}, nil, nil), call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeDelete), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, stub.UniqueError(1)), - call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), + call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil), call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, stub.UniqueError(0)), }, errors.Join( &OpError{Op: "xhost", Err: stub.UniqueError(1), Revert: true}, @@ -247,17 +250,18 @@ func TestCommitRevert(t *testing.T) { {"success", func(sys *I) { sys. - Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711). + Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711, false). ChangeHosts("chronos") }, 0xff, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), + call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil), + call("stat", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, os.ErrNotExist), call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil), call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil), call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil), }, nil, []stub.Call{ call("verbosef", stub.ExpectArgs{"deleting entry %s from X11", []any{xhostOp("chronos")}}, nil, nil), call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeDelete), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil), - call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), + call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true, false}}}, nil, nil), call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, nil), }, nil}, } diff --git a/internal/system/wayland.go b/internal/system/wayland.go index c22c3fdf..e86c3159 100644 --- a/internal/system/wayland.go +++ b/internal/system/wayland.go @@ -42,7 +42,7 @@ func (w *waylandOp) apply(sys *I) (err error) { return newOpError("wayland", err, false) } - if err = sys.aclUpdate(w.dst.String(), sys.uid, acl.Read, acl.Write, acl.Execute); err != nil { + if err = sys.aclUpdate(w.dst.String(), sys.uid, acl.Read|acl.Write|acl.Execute); err != nil { if closeErr := w.ctx.Close(); closeErr != nil { return newOpError("wayland", errors.Join(err, closeErr), false) } diff --git a/internal/system/wayland_test.go b/internal/system/wayland_test.go index 76e6cc93..a9d29378 100644 --- a/internal/system/wayland_test.go +++ b/internal/system/wayland_test.go @@ -32,7 +32,7 @@ func TestWaylandOp(t *testing.T) { call("waylandNew", stub.ExpectArgs{m("/run/user/1971/wayland-0"), m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"}, nil, nil), call("verbosef", stub.ExpectArgs{"wayland pathname socket on %q via %q", []any{m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0")}}, nil, nil), call("chmod", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", os.FileMode(0)}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, stub.UniqueError(2)), + call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, acl.Read | acl.Write | acl.Execute}, nil, stub.UniqueError(2)), }, &OpError{Op: "wayland", Err: stub.UniqueError(2)}, nil, nil}, {"success", 0xbeef, 0xff, &waylandOp{nil, @@ -44,7 +44,7 @@ func TestWaylandOp(t *testing.T) { call("waylandNew", stub.ExpectArgs{m("/run/user/1971/wayland-0"), m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"}, nil, nil), call("verbosef", stub.ExpectArgs{"wayland pathname socket on %q via %q", []any{m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0")}}, nil, nil), call("chmod", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", os.FileMode(0)}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil), + call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, acl.Read | acl.Write | acl.Execute}, nil, nil), }, nil, []stub.Call{ call("verbosef", stub.ExpectArgs{"hanging up wayland socket on %q", []any{m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland")}}, nil, nil), }, nil}, diff --git a/internal/testsuite/fs.go b/internal/testsuite/fs.go new file mode 100644 index 00000000..9acd24b6 --- /dev/null +++ b/internal/testsuite/fs.go @@ -0,0 +1,129 @@ +package testsuite + +import ( + "errors" + "fmt" + "io/fs" + "path/filepath" + "strings" +) + +var ( + // ErrFSBadLength is returned by [FS.Compare] for a directory with an + // unexpected amount of dents. + ErrFSBadLength = errors.New("bad dir length") + // ErrFSBadData is returned by [FS.Compare] for a file with unexpected + // contents. + ErrFSBadData = errors.New("data differs") + // ErrFSBadMode is returned by [FS.Compare] for an entry with unexpected + // mode. + ErrFSBadMode = errors.New("mode differs") + // ErrFSInvalidEnt is returned by [FS.Compare] if an invalid [FS] is visited. + ErrFSInvalidEnt = errors.New("invalid entry condition") +) + +// FS represents part of a filesystem hierarchy. +type FS struct { + // Expected mode of corresponding entry. + Mode fs.FileMode `json:"mode"` + // Expected directory contents. The directory is not descended if Dir is nil. + Dir map[string]*FS `json:"dir"` + // Expected file contents. The file is not read if Data is nil. + Data *string `json:"data"` +} + +// dprintf calls printf if it is non-nil. +func dprintf(printf func(format string, a ...any), format string, a ...any) { + if printf == nil { + return + } + printf(format, a...) +} + +// printDir prints a failed [FS.Compare] directory. +func printDir( + printf func(format string, a ...any), + prefix string, + dir []fs.DirEntry, +) { + names := make([]string, len(dir)) + for i, ent := range dir { + name := ent.Name() + if ent.IsDir() { + name += "/" + } + names[i] = fmt.Sprintf("%q", name) + } + dprintf(printf, "[FAIL] d %s: %s", prefix, strings.Join(names, " ")) +} + +// Compare compares the contents of prefix against the hierarchy described by s. +func (s *FS) Compare( + printf func(format string, a ...any), + prefix string, + e fs.FS, +) error { + if s.Data != nil { + if s.Dir != nil { + panic("invalid state") + } + panic("invalid compare call") + } + + if s.Dir == nil { + dprintf(printf, "[ OK ] s %s", prefix) + return nil + } + + var dir []fs.DirEntry + if d, err := fs.ReadDir(e, prefix); err != nil { + return err + } else if len(d) != len(s.Dir) { + printDir(printf, prefix, d) + return ErrFSBadLength + } else { + dir = d + } + + for _, got := range dir { + name := got.Name() + + if want, ok := s.Dir[name]; !ok { + printDir(printf, prefix, dir) + return fs.ErrNotExist + } else if want.Dir != nil && !got.IsDir() { + printDir(printf, prefix, dir) + return ErrFSInvalidEnt + } else { + name = filepath.Join(prefix, name) + + if fi, err := got.Info(); err != nil { + return err + } else if fi.Mode() != want.Mode { + dprintf(printf, "[FAIL] m %s: %#o, want %#o", + name, uint32(fi.Mode()), uint32(want.Mode)) + return ErrFSBadMode + } + + if want.Data != nil { + if want.Dir != nil { + panic("invalid state") + } + if v, err := fs.ReadFile(e, name); err != nil { + return err + } else if string(v) != *want.Data { + dprintf(printf, + "[FAIL] f %s\n\t got: %s\n\twant: %s", + name, v, *want.Data, + ) + return ErrFSBadData + } + dprintf(printf, "[ OK ] f %s", name) + } else if err := want.Compare(printf, name, e); err != nil { + return err + } + } + } + dprintf(printf, "[ OK ] d %s", prefix) + return nil +} diff --git a/internal/testsuite/fs_test.go b/internal/testsuite/fs_test.go new file mode 100644 index 00000000..4eea866d --- /dev/null +++ b/internal/testsuite/fs_test.go @@ -0,0 +1,85 @@ +package testsuite_test + +import ( + "bytes" + "errors" + "fmt" + "io/fs" + "testing" + "testing/fstest" + + "hakurei.app/internal/testsuite" +) + +func TestCompare(t *testing.T) { + var ( + fsPasswdSample = "u0_a20:x:65534:65534:Hakurei:/var/lib/persist/module/hakurei/u0/a20:/run/current-system/sw/bin/zsh" + fsGroupSample = "hakurei:x:65534:" + ) + + testCases := []struct { + name string + + sample fstest.MapFS + want *testsuite.FS + wantOut string + wantErr error + }{ + {"skip", fstest.MapFS{}, &testsuite.FS{}, "[ OK ] s .\x00", nil}, + {"simple pass", fstest.MapFS{".hakurei": {Mode: 0x800001ed}}, + &testsuite.FS{Dir: map[string]*testsuite.FS{".hakurei": {Mode: 0x800001ed}}}, + "[ OK ] s .hakurei\x00[ OK ] d .\x00", nil}, + {"bad length", fstest.MapFS{".hakurei": {Mode: 0x800001ed}}, + &testsuite.FS{Dir: make(map[string]*testsuite.FS)}, + "[FAIL] d .: \".hakurei/\"\x00", testsuite.ErrFSBadLength}, + {"top level bad mode", fstest.MapFS{".hakurei": {Mode: 0x800001ed}}, + &testsuite.FS{Dir: map[string]*testsuite.FS{".hakurei": {Mode: 0xdeadbeef}}}, + "[FAIL] m .hakurei: 020000000755, want 033653337357\x00", testsuite.ErrFSBadMode}, + {"invalid entry condition", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}}, + &testsuite.FS{Dir: map[string]*testsuite.FS{"test": {Dir: make(map[string]*testsuite.FS)}}}, + "[FAIL] d .: \"test\"\x00", testsuite.ErrFSInvalidEnt}, + {"nonexistent", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}}, + &testsuite.FS{Dir: map[string]*testsuite.FS{".test": {}}}, + "[FAIL] d .: \"test\"\x00", fs.ErrNotExist}, + {"file", fstest.MapFS{"etc": {Mode: 0x800001c0}, + "etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644}, + "etc/group": {Data: []byte(fsGroupSample), Mode: 0644}, + }, &testsuite.FS{Dir: map[string]*testsuite.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*testsuite.FS{ + "passwd": {Mode: 0x1a4, Data: &fsPasswdSample}, + "group": {Mode: 0x1a4, Data: &fsGroupSample}, + }}}}, "[ OK ] f etc/group\x00[ OK ] f etc/passwd\x00[ OK ] d etc\x00[ OK ] d .\x00", nil}, + {"file differ", fstest.MapFS{"etc": {Mode: 0x800001c0}, + "etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644}, + "etc/group": {Data: []byte(fsGroupSample), Mode: 0644}, + }, &testsuite.FS{Dir: map[string]*testsuite.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*testsuite.FS{ + "passwd": {Mode: 0x1a4, Data: &fsGroupSample}, + "group": {Mode: 0x1a4, Data: &fsGroupSample}, + }}}}, "[ OK ] f etc/group\x00[FAIL] f etc/passwd\n\t got: u0_a20:x:65534:65534:Hakurei:/var/lib/persist/module/hakurei/u0/a20:/run/current-system/sw/bin/zsh\n\twant: hakurei:x:65534:\x00", testsuite.ErrFSBadData}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var buf bytes.Buffer + + err := tc.want.Compare( + func(format string, a ...any) { + _, _ = fmt.Fprintf(&buf, format+"\x00", a...) + }, + ".", tc.sample, + ) + if !errors.Is(err, tc.wantErr) { + t.Errorf( + "Compare: error = %v; wantErr %v", + err, tc.wantErr, + ) + } + + if buf.String() != tc.wantOut { + t.Errorf( + "Compare: output %q; want %q", + &buf, tc.wantOut, + ) + } + }) + } +} diff --git a/internal/testsuite/mountinfo/mountinfo.go b/internal/testsuite/mountinfo/mountinfo.go new file mode 100644 index 00000000..19892cd2 --- /dev/null +++ b/internal/testsuite/mountinfo/mountinfo.go @@ -0,0 +1,177 @@ +// Package mountinfo provides util-linux bindings for parsing +// proc_pid_mountinfo(5). +// +// This package must never be used outside integration tests, a much better +// implementation can be found in package vfs. +// +// Attempting to import this package outside testing causes the resulting +// program to panic. +package mountinfo + +/* +#cgo linux pkg-config: --static mount + +#include <stdlib.h> +#include <stdio.h> +#include <libmount.h> + +const char *HAKUREI_MOUNTINFO_PATH = "/proc/self/mountinfo"; +*/ +import "C" + +import ( + "errors" + "fmt" + "runtime" + "unsafe" +) + +var ( + // ErrParse is returned by [Open] when encountering a bad record. + ErrParse = errors.New("invalid mountinfo record") + // ErrIter is returned by [Open] if an iterator cannot be allocated. + ErrIter = errors.New("cannot allocate iterator") + // ErrIterAdvance is stored when the iterator is unable to advance. + ErrIterAdvance = errors.New("unable to advance iterator") +) + +type ( + // Iter refers to libmnt iterator state. + Iter struct { + // Last stored error. + err error + // Whether iteration has concluded. + ok bool + // Whether Close had already been called. + closed bool + + tb *C.struct_libmnt_table + itr *C.struct_libmnt_iter + + fs *C.struct_libmnt_fs + } + + // Entry represents deterministic mountinfo parts of a libmnt_fs entry. + Entry struct { + // mount ID: a unique ID for the mount (may be reused after umount(2)). + ID int `json:"id"` + // parent ID: the ID of the parent mount (or of self for the root of + // this mount namespace's mount tree). + Parent int `json:"parent"` + // root: the pathname of the directory in the filesystem which forms the + // root of this mount. + Root string `json:"root"` + // mount point: the pathname of the mount point relative to the + // process's root directory. + Target string `json:"target"` + // mount options: per-mount options (see mount(2)). + VfsOptstr string `json:"vfs_optstr"` + // filesystem type: the filesystem type in the form "type[.subtype]". + FsType string `json:"fstype"` + // mount source: filesystem-specific information or "none". + Source string `json:"source"` + // super options: per-superblock options (see mount(2)). + FsOptstr string `json:"fs_optstr"` + } +) + +// Copy populates v with the current record. +func (m *Iter) Copy(v *Entry) { + if m.fs == nil { + panic("invalid entry") + } + v.ID = int(C.mnt_fs_get_id(m.fs)) + v.Parent = int(C.mnt_fs_get_parent_id(m.fs)) + v.Root = C.GoString(C.mnt_fs_get_root(m.fs)) + v.Target = C.GoString(C.mnt_fs_get_target(m.fs)) + v.VfsOptstr = C.GoString(C.mnt_fs_get_vfs_options(m.fs)) + v.FsType = C.GoString(C.mnt_fs_get_fstype(m.fs)) + v.Source = C.GoString(C.mnt_fs_get_source(m.fs)) + v.FsOptstr = C.GoString(C.mnt_fs_get_fs_options(m.fs)) +} + +// Err returns the saved iterator error. +func (m *Iter) Err() error { return m.err } + +// Open opens a mountinfo document. If name is an empty string, the mountinfo +// document of the current process is opened instead. +func Open(name string) (*Iter, error) { + var m Iter + if name == "" { + m.tb = C.mnt_new_table_from_file(C.HAKUREI_MOUNTINFO_PATH) + } else { + _name := C.CString(name) + m.tb = C.mnt_new_table_from_file(_name) + C.free(unsafe.Pointer(_name)) + } + if m.tb == nil { + return nil, ErrParse + } + m.itr = C.mnt_new_iter(C.MNT_ITER_FORWARD) + if m.itr == nil { + C.mnt_unref_table(m.tb) + return nil, ErrIter + } + m.ok = true + + runtime.SetFinalizer(&m, (*Iter).Close) + return &m, nil +} + +// Close frees the iterator. +func (m *Iter) Close() { + if m.closed { + return + } + if m.tb == nil { + panic("unref called before open") + } + + C.mnt_unref_table(m.tb) + C.mnt_free_iter(m.itr) + m.closed = true + runtime.SetFinalizer(m, nil) +} + +// Reset resets the iterator to the first record for reuse. +func (m *Iter) Reset() { + if m.err != nil { + panic("attempting to reset a faulted iterator") + } + m.ok = true + C.mnt_reset_iter(m.itr, -1) +} + +// Next advances the iterator to the next record. The record may be copied if +// Next returns true. +func (m *Iter) Next() bool { + if !m.ok || m.err != nil { + return false + } + + r := C.mnt_table_next_fs(m.tb, m.itr, &m.fs) + if r < 0 { + m.err = ErrIterAdvance + } + m.ok = r == 0 + return m.ok +} + +// EqualWithIgnore compares e with want, ignoring fields with the specified +// ignore value. +func (e *Entry) EqualWithIgnore(want *Entry, ignore string) bool { + return (e.ID == want.ID || want.ID == -1) && + (e.Parent == want.Parent || want.Parent == -1) && + (e.Root == want.Root || want.Root == ignore) && + (e.Target == want.Target || want.Target == ignore) && + (e.VfsOptstr == want.VfsOptstr || want.VfsOptstr == ignore) && + (e.FsType == want.FsType || want.FsType == ignore) && + (e.Source == want.Source || want.Source == ignore) && + (e.FsOptstr == want.FsOptstr || want.FsOptstr == ignore) +} + +// String returns a text representation of e loosely following the kernel format. +func (e *Entry) String() string { + return fmt.Sprintf("%d %d %s %s %s %s %s %s", + e.ID, e.Parent, e.Root, e.Target, e.VfsOptstr, e.FsType, e.Source, e.FsOptstr) +} diff --git a/internal/testsuite/mountinfo/mountinfo_guard.go b/internal/testsuite/mountinfo/mountinfo_guard.go new file mode 100644 index 00000000..aaf63ac4 --- /dev/null +++ b/internal/testsuite/mountinfo/mountinfo_guard.go @@ -0,0 +1,15 @@ +//go:build !testsuite && !tester + +package mountinfo + +import ( + "os" + "testing" +) + +func init() { + if !testing.Testing() { + println("package mountinfo imported in non-testsuite program") + os.Exit(1) + } +} diff --git a/test/internal/sandbox/mount_test.go b/internal/testsuite/mountinfo/mountinfo_test.go index a565b0a0..93be3880 100644 --- a/test/internal/sandbox/mount_test.go +++ b/internal/testsuite/mountinfo/mountinfo_test.go @@ -1,13 +1,11 @@ -//go:build testtool - -package sandbox_test +package mountinfo_test import ( "os" "path/filepath" "testing" - "hakurei.app/test/internal/sandbox" + "hakurei.app/internal/testsuite/mountinfo" ) func TestMountinfo(t *testing.T) { @@ -15,7 +13,7 @@ func TestMountinfo(t *testing.T) { name string sample string - want []*sandbox.MountinfoEntry + want []*mountinfo.Entry }{ {"util-linux", `15 20 0:3 / /proc rw,relatime - proc /proc rw 16 20 0:15 / /sys rw,relatime - sysfs /sys rw @@ -49,7 +47,7 @@ func TestMountinfo(t *testing.T) { 44 41 0:36 / /home/kzak/.gvfs rw,nosuid,nodev,relatime - fuse.gvfs-fuse-daemon gvfs-fuse-daemon rw,user_id=500,group_id=500 45 20 0:37 / /var/lib/nfs/rpc_pipefs rw,relatime - rpc_pipefs sunrpc rw 47 20 0:38 / /mnt/sounds rw,relatime - cifs //foo.home/bar/ rw,unc=\\foo.home\bar,username=kzak,domain=SRGROUP,uid=0,noforceuid,gid=0,noforcegid,addr=192.168.111.1,posixpaths,serverino,acl,rsize=16384,wsize=57344 -49 20 0:56 / /mnt/test/foobar rw,relatime,nosymfollow shared:323 - tmpfs tmpfs rw`, []*sandbox.MountinfoEntry{ +49 20 0:56 / /mnt/test/foobar rw,relatime,nosymfollow shared:323 - tmpfs tmpfs rw`, []*mountinfo.Entry{ e(15, 20, "/", "/proc", "rw,relatime", "proc", "/proc", "rw"), e(16, 20, "/", "/sys", "rw,relatime", "sysfs", "/sys", "rw"), e(17, 20, "/", "/dev", "rw,relatime", "devtmpfs", "udev", "rw,size=1983516k,nr_inodes=495879,mode=755"), @@ -93,33 +91,35 @@ func TestMountinfo(t *testing.T) { } t.Run(tc.name, func(t *testing.T) { - m := sandbox.NewMountinfo(name) - if err := m.Parse(); err != nil { - t.Fatalf("Parse: error = %v", err) + m, err := mountinfo.Open(name) + if err != nil { + t.Fatalf("Open: error = %v", err) } + t.Cleanup(m.Close) i := 0 - for ent := range m.Entries() { + var ent mountinfo.Entry + for m.Next() { + m.Copy(&ent) + if i == len(tc.want) { - t.Errorf("Entries: got more than %d entries", i) + t.Errorf("Next: got more than %d entries", i) t.FailNow() } if !ent.EqualWithIgnore(tc.want[i], "\x00") { - t.Errorf("Entries: entry %d\n got: %#v\nwant: %#v", i, + t.Errorf("Next: entry %d\n got: %#v\nwant: %#v", i, ent, &tc.want[i]) t.FailNow() } else { - t.Logf("%s", ent) + t.Logf("%s", &ent) } i++ } - if err := m.Err(); err != nil { - t.Fatalf("Mountinfo: error = %v", err) + if err = m.Err(); err != nil { + t.Fatalf("Err: %v", err) } - - m.Unref() }) if err := os.Remove(name); err != nil { @@ -129,9 +129,11 @@ func TestMountinfo(t *testing.T) { } func e( - id, parent int, root, target, vfsOptstr string, fsType, source, fsOptstr string, -) *sandbox.MountinfoEntry { - return &sandbox.MountinfoEntry{ + id, parent int, + root, target, vfsOptstr string, + fsType, source, fsOptstr string, +) *mountinfo.Entry { + return &mountinfo.Entry{ ID: id, Parent: parent, Root: root, diff --git a/internal/testsuite/proc.go b/internal/testsuite/proc.go new file mode 100644 index 00000000..e7ef1aed --- /dev/null +++ b/internal/testsuite/proc.go @@ -0,0 +1,353 @@ +package testsuite + +import ( + "bytes" + "errors" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" + "unsafe" + + "hakurei.app/fhs" +) + +// Stat represents status information read from /proc/pid/stat. +type Stat struct { + // The process ID. + PID int + // The filename of the executable, with parenthesis stripped. + Comm string + // One of the following characters, indicating process state: + // + // R Running + // + // S Sleeping in an interruptible wait + // + // D Waiting in uninterruptible disk sleep + // + // Z Zombie + // + // T Stopped (on a signal) or (before Linux + // 2.6.33) trace stopped + // + // t Tracing stop (Linux 2.6.33 onward) + // + // W Paging (only before Linux 2.6.0) + // + // X Dead (from Linux 2.6.0 onward) + // + // x Dead (Linux 2.6.33 to 3.13 only) + // + // K Wakekill (Linux 2.6.33 to 3.13 only) + // + // W Waking (Linux 2.6.33 to 3.13 only) + // + // P Parked (Linux 3.9 to 3.13 only) + // + // I Idle (Linux 4.14 onward) + State byte + // The process ID of the parent of this process. + PPID int + // The process group ID of the process. + PGRP int + // The session ID of the process. + Session int + // The controlling terminal of the process. + TTYNR int + // The ID of the foreground process group of the controlling terminal of the + // process. + TPGID int + // The kernel flags word of the process. For bit meanings, see the PF_* + // defines in the Linux kernel source file include/linux/sched.h. + Flags uint + // The number of minor faults the process has made which have not required + // loading a memory page from disk. + MinFlt uint + // The number of minor faults that the process's waited-for children have + // made. + CMinFlt uint + // The number of major faults the process has made which have required + // loading a memory page from disk. + MajFlt uint + // The number of major faults that the process's waited-for children have + // made. + CMajFlt uint + // Amount of time that this process has been scheduled in user mode, + // measured in clock ticks. + UTime uint + // Amount of time that this process has been scheduled in kernel mode, + // measured in clock ticks. + STime uint + // Amount of time that this process's waited-for children have been + // scheduled in user mode, measured in clock ticks. + CUTime int + // Amount of time that this process's waited-for children have been + // scheduled in kernel mode, measured in clock ticks. + CSTime int + // For processes running a real-time scheduling policy, this is the negated + // scheduling priority, minus one. + Priority int + // The nice value, a value in the range 19 (low priority) to -20 (high + // priority). + Nice int + // Number of threads in this process. + NumThreads int + + // unmaintained field: itrealvalue + + // The time the process started after system boot. Since Linux 2.6, the + // value is expressed in clock ticks. + StartTime uint64 + // Virtual memory size in bytes. + VSize uint + // Resident set size in pages. + RSS int + // Soft limit in bytes on the rss of the process. + RSSLim uint64 + // The address above which program text can run. + StartCode uint64 + // The address below which program text can run. + EndCode uint64 + // The address of the start (i.e., bottom) of the stack. + StartStack uint64 + // The current value of ESP (stack pointer), as found in the kernel stack + // page for the process. + KSTKESP uint64 + // The current EIP (instruction pointer). + KSTKEIP uint64 + + // obsolete fields: signal, blocked, sigignore, sigcatch + + // This is the "channel" in which the process is waiting. It is the address + // of a location in the kernel where the process is sleeping. + WChan uint64 + + // unmaintained fields: nswap, cnswap + + // Signal to be sent to parent when we die. + ExitSignal int + // CPU number last executed on. + Processor int + // Real-time scheduling priority, a number in the range 1 to 99 for processes + // scheduled under a real-time policy, or 0, for non-real-time processes. + RTPriority uint + // Scheduling policy (see sched_setscheduler(2)). Decode using the SCHED_* + // constants in linux/sched.h. + Policy uint + // Aggregated block I/O delays, measured in clock ticks (centiseconds). + DelayAcctBlkIOTicks uint64 + // Guest time of the process (time spent running a virtual CPU for a guest + // operating system), measured in clock ticks. + GuestTime int + // Guest time of the process's children, measured in clock ticks. + CGuestTime int +} + +// Executable is like [os.Executable], but for the process referred to by s. +func (s *Stat) Executable() (string, error) { + path, err := os.Readlink(filepath.Join(fhs.Proc, strconv.Itoa(s.PID), "exe")) + + // When the executable has been deleted then Readlink returns a + // path appended with " (deleted)". + return strings.TrimSuffix(path, " (deleted)"), err +} + +// Stat populates stat with the proc filesystem entry referred to by s. +func (s *Stat) Stat(stat *syscall.Stat_t) (err error) { + err = syscall.Stat(filepath.Join(fhs.Proc, strconv.Itoa(s.PID)), stat) + if err != nil { + err = os.NewSyscallError("stat", err) + } + return +} + +// Args reads arguments of the process referred to by s. +func (s *Stat) Args() ([]string, error) { + p, err := os.ReadFile(filepath.Join(fhs.Proc, strconv.Itoa(s.PID), "cmdline")) + if err != nil { + return nil, err + } + a := bytes.Split(p, []byte{0}) + if len(a) > 0 && len(a[len(a)-1]) == 0 { + a = a[:len(a)-1] + } + + args := make([]string, len(a)) + for i, arg := range a { + args[i] = unsafe.String(unsafe.SliceData(arg), len(arg)) + } + return args, nil +} + +// ErrBadDelimiters is returned by [Stat.UnmarshalText] if one or both bytes of +// the comm delimiter pair were missing or misplaced. +var ErrBadDelimiters = errors.New("missing comm delimiters") + +// UnmarshalText populates the structure pointed to by s from text. +func (s *Stat) UnmarshalText(text []byte) (err error) { + var ( + discard uint64 + _uint64 = &discard + _int64 = (*int64)(unsafe.Pointer(&discard)) + + ld = bytes.Index(text, []byte("(")) + rd = bytes.LastIndex(text, []byte(")")) + ) + + if ld <= 0 || rd < 0 { + return ErrBadDelimiters + } + + if s.PID, err = strconv.Atoi( + unsafe.String(unsafe.SliceData(text), ld-1), + ); err != nil { + return + } + + s.Comm = string(text[ld+1 : rd]) + + var ( + n int + + state string + ) + n, err = fmt.Fscan( + bytes.NewBuffer(text[rd+2:]), + &state, + &s.PPID, + &s.PGRP, + &s.Session, + &s.TTYNR, + &s.TPGID, + &s.Flags, + &s.MinFlt, + &s.CMinFlt, + &s.MajFlt, + &s.CMajFlt, + &s.UTime, + &s.STime, + &s.CUTime, + &s.CSTime, + &s.Priority, + &s.Nice, + &s.NumThreads, + _int64, + &s.StartTime, + &s.VSize, + &s.RSS, + &s.RSSLim, + &s.StartCode, + &s.EndCode, + &s.StartStack, + &s.KSTKESP, + &s.KSTKEIP, + _uint64, + _uint64, + _uint64, + _uint64, + &s.WChan, + _uint64, + _uint64, + &s.ExitSignal, + &s.Processor, + &s.RTPriority, + &s.Policy, + &s.DelayAcctBlkIOTicks, + &s.GuestTime, + &s.CGuestTime, + ) + if err != nil { + err = fmt.Errorf("field %d: %w", n, err) + } else if len(state) != 1 { + err = fmt.Errorf("invalid state %q", state) + } else { + s.State = state[0] + } + return +} + +// A StatScanner continuously scans the proc filesystem for process status +// information in /proc/pid/stat. +type StatScanner struct { + // Current entry. + stat Stat + // Cached top-level /proc entries. + dents []os.DirEntry + // Current progress through dents. + i int + // Whether the previous call to Scan had repopulated dents. + wrapped bool + // First stored error: a non-nil err disables the scanner. + err error +} + +// IsNotExist returns whether an error is [os.ErrNotExist] or ESRCH. +func IsNotExist(err error) bool { + return errors.Is(err, os.ErrNotExist) || errors.Is(err, syscall.ESRCH) +} + +// Scan reads a process status information entry. It returns false if an +// unrecoverable error is encountered, after which Scan no longer scans new +// entries. +func (s *StatScanner) Scan() bool { + if s.err != nil { + return false + } + + if s.wrapped = s.i == len(s.dents); s.wrapped { + if s.dents, s.err = os.ReadDir(fhs.Proc); s.err != nil { + return false + } + s.i = 0 + if len(s.dents) == 0 { + s.err = syscall.ENOTRECOVERABLE + return false + } + } + + for s.i < len(s.dents) { + dent := s.dents[s.i] + s.i++ + if !dent.IsDir() { + continue + } + + pid, err := strconv.Atoi(dent.Name()) + if err != nil { + continue + } + + var p []byte + p, err = os.ReadFile(filepath.Join(fhs.Proc, dent.Name(), "stat")) + if err != nil { + if IsNotExist(err) { + continue + } + s.err = err + return false + } + + s.err = s.stat.UnmarshalText(p) + if s.err == nil && pid != s.stat.PID { + s.err = fmt.Errorf( + "bad status information: dent=%d, stat=%d", + pid, s.stat.PID, + ) + } + return s.err == nil + } + return s.Scan() +} + +// Stat returns the address of the [Stat] structure populated by the last call +// to Scan. +func (s *StatScanner) Stat() *Stat { return &s.stat } + +// Err returns the stored error value. +func (s *StatScanner) Err() error { return s.err } + +// Repopulated returns whether the last Scan call had re-read the proc filesystem. +func (s *StatScanner) Repopulated() bool { return s.wrapped } diff --git a/internal/testsuite/proc_test.go b/internal/testsuite/proc_test.go new file mode 100644 index 00000000..73693972 --- /dev/null +++ b/internal/testsuite/proc_test.go @@ -0,0 +1,33 @@ +package testsuite_test + +import ( + "testing" + + "hakurei.app/internal/testsuite" +) + +func BenchmarkStatScanner(b *testing.B) { + var s testsuite.StatScanner + + for b.Loop() { + if !s.Scan() { + b.Fatal(s.Err()) + } + } +} + +func BenchmarkStatScannerFull(b *testing.B) { + var s testsuite.StatScanner + + for b.Loop() { + for s.Scan() { + if s.Repopulated() { + break + } + } + + if err := s.Err(); err != nil { + b.Fatal(err) + } + } +} diff --git a/internal/testsuite/ptrace.go b/internal/testsuite/ptrace.go new file mode 100644 index 00000000..d7abd0aa --- /dev/null +++ b/internal/testsuite/ptrace.go @@ -0,0 +1,151 @@ +package testsuite + +import ( + "crypto/sha512" + "encoding/base64" + "errors" + "fmt" + "os" + "runtime" + "syscall" + "unsafe" +) + +const ( + // _PTRACE_ATTACH attaches to the process specified in pid. + _PTRACE_ATTACH = 16 + // _PTRACE_DETACH restarts the stopped tracee as for PTRACE_CONT, but first + // detaches from it. + _PTRACE_DETACH = 17 + + // _PTRACE_SECCOMP_GET_FILTER allows the tracer to dump the tracee's classic + // BPF filters. + _PTRACE_SECCOMP_GET_FILTER = 0x420c +) + +// ptrace wraps the ptrace syscall. +func ptrace( + op uintptr, + pid, addr int, + data unsafe.Pointer, +) (r uintptr, errno syscall.Errno) { + r, _, errno = syscall.Syscall6( + syscall.SYS_PTRACE, + op, + uintptr(pid), + uintptr(addr), + uintptr(data), + 0, 0, + ) + return +} + +// ptraceAttach attaches to the process referred to by pid. +func ptraceAttach(pid int) error { + if _, errno := ptrace(_PTRACE_ATTACH, pid, 0, nil); errno != 0 { + return os.NewSyscallError("PTRACE_ATTACH", errno) + } + + var status syscall.WaitStatus + for { + if _, err := syscall.Wait4( + pid, + &status, + syscall.WALL, + nil, + ); err != nil { + if errors.Is(err, syscall.EINTR) { + continue + } + return os.NewSyscallError("wait4", err) + } + switch { + case status.Stopped(): + return nil + + case status.Continued(): + continue + + case status.Signaled(): + return fmt.Errorf( + "tracee terminated by signal %s", + status.Signal(), + ) + + case status.Exited(): + return fmt.Errorf( + "tracee terminated unexpectedly with code %d", + status.ExitStatus(), + ) + } + } +} + +// ptraceDetach detaches from the attached process referred to by pid. +func ptraceDetach(pid int) error { + if _, errno := ptrace(_PTRACE_DETACH, pid, 0, nil); errno != 0 { + return os.NewSyscallError("PTRACE_DETACH", errno) + } + return nil +} + +// getFilter dumps the specified tracee's cBPF filter at the specified index +// and returns the resulting payload. T must be eight bytes long and must not +// contain pointers. +func getFilter(pid, index int) ([]syscall.SockFilter, error) { + var buf []syscall.SockFilter + if n, errno := ptrace( + _PTRACE_SECCOMP_GET_FILTER, + pid, index, nil, + ); errno != 0 { + return nil, os.NewSyscallError("PTRACE_SECCOMP_GET_FILTER", errno) + } else { + buf = make([]syscall.SockFilter, n) + } + if _, errno := ptrace( + _PTRACE_SECCOMP_GET_FILTER, + pid, index, unsafe.Pointer(&buf[0]), + ); errno != 0 { + return nil, os.NewSyscallError("PTRACE_SECCOMP_GET_FILTER", errno) + } + return buf, nil +} + +// CheckFilter checks the process at pid to have its first filter's contents +// match the specified sha512 checksum. +func CheckFilter(pid, index int, sum [sha512.Size]byte) (err error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + if err = ptraceAttach(pid); err != nil { + return + } + defer func() { + if detachErr := ptraceDetach(pid); err == nil { + err = detachErr + if errors.Is(err, syscall.ESRCH) { + err = nil + } + } + }() + + var buf []syscall.SockFilter + h := sha512.New() + if buf, err = getFilter(pid, index); err != nil { + return + } else { + h.Write(unsafe.Slice( + (*byte)(unsafe.Pointer(&buf[0])), + uintptr(len(buf))*unsafe.Sizeof(buf[0]), + )) + } + + if got := h.Sum(nil); string(got) != string(sum[:]) { + return fmt.Errorf( + "bad filter\n\t got: %s\n\twant: %s", + base64.StdEncoding.EncodeToString(got), + base64.StdEncoding.EncodeToString(sum[:]), + ) + } + return +} diff --git a/internal/testsuite/ptrace_test.go b/internal/testsuite/ptrace_test.go new file mode 100644 index 00000000..18ad3f31 --- /dev/null +++ b/internal/testsuite/ptrace_test.go @@ -0,0 +1,13 @@ +package testsuite_test + +import ( + "syscall" + "testing" + "unsafe" +) + +func TestBlockSize(t *testing.T) { + if sz := unsafe.Sizeof(syscall.SockFilter{}); sz != 8 { + t.Fatalf("invalid filter block size %d", sz) + } +} diff --git a/internal/testsuite/testsuite.go b/internal/testsuite/testsuite.go new file mode 100644 index 00000000..395bf8f2 --- /dev/null +++ b/internal/testsuite/testsuite.go @@ -0,0 +1,910 @@ +// Package testsuite provides many quick-and-dirty integration testing utilities. +// +// Attempting to import this package outside testing causes the resulting +// program to panic. +package testsuite + +import ( + "bufio" + "context" + "crypto/sha512" + "encoding/json" + "errors" + "fmt" + "io" + "iter" + "log" + "os" + "os/exec" + "os/signal" + "os/user" + "path/filepath" + "reflect" + "runtime" + "slices" + "strconv" + "strings" + "sync" + "sync/atomic" + "syscall" + "time" + + "hakurei.app/check" + "hakurei.app/fhs" + "hakurei.app/hst" + "hakurei.app/internal/store" +) + +// serial is the next value returned by [NewSerial]. +var serial atomic.Uint64 + +// NewSerial returns a serial number. +func NewSerial() uint64 { return serial.Add(1) - 1 } + +// ReceiveSignals blocks until a termination signal arrives, and terminates. +func ReceiveSignals() { + s := make(chan os.Signal, 3) + signal.Notify(s, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) + log.Fatalf("terminating on signal %s", <-s) +} + +var ( + // users caches [user.LookupId] calls. + users = make(map[string]*user.User) + // usersMu synchronises access to users. + usersMu sync.RWMutex +) + +// mustLookupId is like [user.LookupId], but the first result is cached. +func mustLookupId(uid string) *user.User { + usersMu.RLock() + v, ok := users[uid] + usersMu.RUnlock() + if ok { + return v + } + + u, err := user.LookupId(uid) + if err != nil { + log.Fatal(err) + } + + usersMu.Lock() + users[uid] = u + usersMu.Unlock() + return u +} + +// MustAppendEnv adds extra environment variables to cmd. +func MustAppendEnv(cmd *exec.Cmd, env ...string) { + if len(cmd.Env) == 0 { + var cred syscall.Credential + if cmd.SysProcAttr != nil && cmd.SysProcAttr.Credential != nil { + cred = *cmd.SysProcAttr.Credential + } + u := mustLookupId(strconv.Itoa(int(cred.Uid))) + + cmd.Env = append(cmd.Env, + "PATH="+os.Getenv("PATH"), + "HOME="+u.HomeDir, + "USER="+u.Username, + "USERNAME="+u.Username, + ) + } + cmd.Env = append(cmd.Env, env...) +} + +// MustRun runs command and terminates the testsuite on error. +func MustRun(cred *syscall.Credential, extraEnv []string, command ...string) { + cmd := exec.Command(command[0], command[1:]...) + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + cmd.SysProcAttr = &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGKILL, + Credential: cred, + } + MustAppendEnv(cmd, extraEnv...) + if err := cmd.Run(); err != nil { + log.Fatalf("must run %s: %v", strings.Join(command, " "), err) + } +} + +// ErrUnexpectedSuccess is returned for processes expected to exit with a +// non-zero code, but failed to do so. +var ErrUnexpectedSuccess = errors.New("process unexpectedly exited with code 0") + +// MustFail runs command and terminates the testsuite if the program fails to +// start or exits with code 0. +func MustFail( + cred *syscall.Credential, + extraEnv []string, + quiet bool, + command ...string, +) { + cmd := exec.Command(command[0], command[1:]...) + if !quiet { + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + } + cmd.SysProcAttr = &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGKILL, + Credential: cred, + } + MustAppendEnv(cmd, extraEnv...) + if err := cmd.Run(); err == nil { + log.Fatal(ErrUnexpectedSuccess) + } else if e, ok := errors.AsType[*exec.ExitError](err); !ok { + log.Fatalf("must fail %s: %v", strings.Join(command, " "), err) + } else if !e.Exited() { + log.Fatalf("must fail %s: %v", strings.Join(command, " "), e) + } +} + +// MustStart starts cmd and returns a channel delivering its wait error. +func MustStart(cmd *exec.Cmd) (done <-chan error) { + if err := cmd.Start(); err != nil { + log.Fatal(err) + } + d := make(chan error, 1) + go func() { d <- cmd.Wait() }() + return d +} + +// MustStartWith wraps [MustStart] and creates the [exec.Cmd] object internally. +func MustStartWith( + ctx context.Context, + cred *syscall.Credential, + quiet bool, + extraEnv []string, + files []*os.File, + command ...string, +) (proc *os.Process, done <-chan error) { + cmd := exec.CommandContext(ctx, command[0], command[1:]...) + if !quiet { + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + } + cmd.ExtraFiles = files + cmd.SysProcAttr = &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGTERM, + Credential: cred, + } + MustAppendEnv(cmd, extraEnv...) + return cmd.Process, MustStart(cmd) +} + +// MustOutput is like [MustRun] and [MustFail], but collects the entire output +// in memory and returns them as strings. +func MustOutput( + ctx context.Context, + cred *syscall.Credential, + extraEnv []string, + exitCode int, + name string, + command ...string, +) (string, string) { + var stdout, stderr strings.Builder + cmd := exec.CommandContext(ctx, command[0], command[1:]...) + cmd.Stdout, cmd.Stderr = &stdout, &stderr + cmd.SysProcAttr = &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGTERM, + Credential: cred, + } + MustAppendEnv(cmd, extraEnv...) + + err := cmd.Run() + if err == nil { + if exitCode == 0 { + return stdout.String(), stderr.String() + } + log.Fatalf( + "%s succeeded unexpectedly"+ + "\nstdout = %q\nstderr = %q", + name, + stdout.String(), + stderr.String(), + ) + } + + if e, ok := errors.AsType[*exec.ExitError](err); !ok { + log.Fatal(err) + } else if e.ExitCode() != exitCode { + log.Fatalf( + "%s: %v"+ + "\nstdout = %q\nstderr = %q", + name, e, + stdout.String(), + stderr.String(), + ) + } + return stdout.String(), stderr.String() +} + +// AssertOutput returns a function that terminates the program if called with +// arguments differing from the ones it is initialised with. +func AssertOutput(action, stdout, stderr string) ( + assert func(stdout, stderr string), +) { + return func(gotStdout, gotStderr string) { + if stdout != gotStdout || stderr != gotStderr { + log.Fatalf( + "unexpected output %s"+ + "\nstdout = %q\nstderr = %q", + action, + gotStdout, + gotStderr, + ) + } + } +} + +// MustCheckFilter is like [CheckFilter], but terminates the test suite if a +// non-nil error is returned. Otherwise, the tracee is terminated after it +// resumes. +func MustCheckFilter(name string, pid int, sum [sha512.Size]byte) { + if err := CheckFilter( + pid, + 1, /* podman installs its own filter */ + sum, + ); err != nil { + log.Fatalln(name, err) + } else if err = syscall.Kill(pid, syscall.SIGTERM); err != nil { + log.Fatalf("cannot terminate tracee: %v", err) + } +} + +// FilterTerminated returns a non-nil error if err is not an [exec.ExitError] +// describing a process terminated by a syscall.SIGTERM signal. +func FilterTerminated(err error) error { + if err == nil { + return ErrUnexpectedSuccess + } + + e, ok := errors.AsType[*exec.ExitError](err) + if !ok { + return err + } + + if e.ExitCode() == 0x80+int(syscall.SIGTERM) { + return nil + } + return e +} + +// MustDecodeFrom runs the specified command and decodes its JSON output, +// storing the result in the value pointed to by v. +func MustDecodeFrom[T any]( + ctx context.Context, + cred *syscall.Credential, + env []string, + v *T, + name string, + arg ...string, +) { + cmd := exec.CommandContext(ctx, name, arg...) + cmd.Env = env + cmd.SysProcAttr = &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGKILL, + Credential: cred, + } + cmd.Stderr = os.Stderr + + r, err := cmd.StdoutPipe() + if err != nil { + log.Fatal(err) + } + + if err = cmd.Start(); err != nil { + log.Fatalf("start %s: %v", name, err) + } + + if err = json.NewDecoder(r).Decode(v); err != nil { + log.Fatalf("decode %s: %v", name, err) + } + + if err = cmd.Wait(); err != nil { + log.Fatalf("wait for %s: %v", name, err) + } +} + +// Poll repeatedly runs command until it succeeds. +func Poll( + d time.Duration, + cred *syscall.Credential, + extraEnv []string, + command ...string, +) { + for range time.NewTicker(d).C { + cmd := exec.Command(command[0], command[1:]...) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGKILL, + Credential: cred, + } + MustAppendEnv(cmd, extraEnv...) + if err := cmd.Run(); err != nil { + if e, ok := errors.AsType[*exec.ExitError](err); ok && e.Exited() { + continue + } + log.Fatalf("poll %s: %v", strings.Join(command, " "), err) + } + break + } +} + +// PollPID polls until the process referred to by pid no longer exists. +func PollPID(name string, pid int) { + for { + time.Sleep(10 * time.Millisecond) + if err := syscall.Kill(pid, 0); err == nil { + continue + } else if errors.Is(err, syscall.ESRCH) { + break + } else { + log.Fatalf("cannot poll for %s: %v", name, err) + } + } +} + +const ( + // XDGRuntimeDir is the hardcoded XDG runtime directory of the only user + // mapped in hsurc. + XDGRuntimeDir = "/var/run/user/1000" + + // XDGRuntimeEnv is the environment variable string for XDG_RUNTIME_DIR. + XDGRuntimeEnv = "XDG_RUNTIME_DIR=" + XDGRuntimeDir + + // HakureiPrefix is the hakurei work directory of the only user mapped in + // hsurc. + HakureiPrefix = "/tmp/hakurei.0/" +) + +// MustPrepareUserDirs creates user-specific directories. +func MustPrepareUserDirs(cred *syscall.Credential) { + if err := os.MkdirAll(XDGRuntimeDir, 0700); err != nil { + log.Fatal(err) + } else if err = os.Chown( + XDGRuntimeDir, + int(cred.Uid), + int(cred.Gid), + ); err != nil { + log.Fatal(err) + } +} + +// SessionEnv returns environment variables referring to the hosted session. +func SessionEnv(dbusEnv string) []string { + return []string{ + "PATH=" + os.Getenv("PATH"), + "TERM=xterm", + XDGRuntimeEnv, + WaylandEnv, + "DISPLAY=:0", + dbusEnv, + } +} + +// quietServices is initialised from the environment and controls whether +// session services should have their outputs connected. +var quietServices = os.Getenv("HAKUREI_VERBOSE_SERVICES") == "" + +// MustStartSessionBus starts a session bus that is never explicitly terminated. +// The test suite is terminated if the session bus daemon terminates. +func MustStartSessionBus(cred *syscall.Credential) (dbusEnv string) { + r, w, err := os.Pipe() + if err != nil { + log.Fatal(err) + } + + // this is never explicitly terminated + _, done := MustStartWith( + context.Background(), cred, quietServices, nil, []*os.File{w}, + "dbus-daemon", + "--print-address=3", + "--address=unix:path="+XDGRuntimeDir+"/dbus", + "--session", + "--nofork", + "--nopidfile", + ) + + go func() { + if _err := <-done; _err != nil { + log.Fatalf("session bus terminated unexpectedly: %v", _err) + } + log.Fatal("session bus terminated unexpectedly") + }() + + dbusEnv, err = bufio.NewReader(r).ReadString('\n') + if err != nil { + log.Fatal(err) + } + dbusEnv = dbusEnv[:len(dbusEnv)-1] + log.Printf("dbus listening on %s", dbusEnv) + dbusEnv = "DBUS_SESSION_BUS_ADDRESS=" + dbusEnv + + if err = r.Close(); err != nil { + log.Fatal(err) + } + return +} + +const ( + // SwayEnv is the environment variable string for the sway IPC socket. + SwayEnv = "SWAYSOCK=" + XDGRuntimeDir + "/sway" + // WaylandEnv is the environment variable string for the wayland display. + WaylandEnv = "WAYLAND_DISPLAY=wayland-1" +) + +// MustStartSway starts the sway wayland display server which must be terminated +// by calling [TerminateSway]. +func MustStartSway( + wg *sync.WaitGroup, + cred *syscall.Credential, + dbusEnv string, +) { + wg.Go(func() { + // this is terminated via swaymsg + _, done := MustStartWith( + context.Background(), cred, quietServices, []string{ + "WLR_BACKENDS=headless", + XDGRuntimeEnv, + SwayEnv, + dbusEnv, + }, nil, + "sway", + ) + if err := <-done; err != nil { + log.Fatalf("sway terminated unexpectedly: %v", err) + } + }) + + Poll( + 50*time.Millisecond, + cred, + []string{SwayEnv}, + "swaymsg", + ) + Poll( + 50*time.Millisecond, + cred, + []string{"DISPLAY=:0"}, + "xset", "q", + ) + log.Printf("sway available via %s", SwayEnv) +} + +// TerminateSway requests for the sway server to terminate via sway IPC. +func TerminateSway(cred *syscall.Credential) { + MustFail(cred, []string{SwayEnv}, quietServices, "swaymsg", "exit") +} + +// SwayNode represents a Sway window with hakurei-specific fields. +type SwayNode struct { + // Window title. + Name string `json:"name"` + // Descendents. + Nodes []SwayNode `json:"nodes"` + // Wayland app id. + AppID string `json:"app_id,omitempty"` + + // Should always be app.hakurei for cmd/hakurei containers. + Sandbox string `json:"sandbox_engine,omitempty"` + // Configured via [hst]. + ID string `json:"sandbox_app_id,omitempty"` + // Hakurei instance identifier. + Instance hst.ID `json:"sandbox_instance_id,omitempty"` +} + +// All returns an iterator over n and all descendents of n. +func (n *SwayNode) All() iter.Seq[*SwayNode] { + return func(yield func(*SwayNode) bool) { + if !yield(n) { + return + } + + for _, d := range n.Nodes { + for v := range d.All() { + if !yield(v) { + return + } + } + } + } +} + +// PollSwayWindow continuously polls sway for a Wayland client to appear. +func PollSwayWindow( + ctx context.Context, + cred *syscall.Credential, + f func(n *SwayNode) bool, +) *SwayNode { + var t SwayNode + for { + time.Sleep(25 * time.Millisecond) + MustDecodeFrom( + ctx, cred, []string{SwayEnv}, &t, + "swaymsg", "-t", "get_tree", + ) + for n := range t.All() { + if f(n) { + return n + } + } + } +} + +// MustStartPipeWire starts a PipeWire server that is never explicitly +// terminated. The test suite is terminated if the PipeWire server terminates. +func MustStartPipeWire(cred *syscall.Credential, dbusEnv string) (version string) { + version, _ = MustOutput( + context.Background(), nil, nil, 0, + "pipewire version", + "pipewire", "--version", + ) + _, version, _ = strings.Cut(version, "Compiled with libpipewire ") + _, version, _ = strings.Cut(version, "\nLinked with libpipewire") + version = strings.TrimSpace(version) + + // this is never explicitly terminated + _, done := MustStartWith( + context.Background(), cred, quietServices, []string{ + XDGRuntimeEnv, + dbusEnv, + }, nil, + "pipewire", + ) + + go func() { + if _err := <-done; _err != nil { + log.Fatalf("pipewire terminated unexpectedly: %v", _err) + } + log.Fatal("pipewire terminated unexpectedly") + }() + + Poll(50*time.Millisecond, cred, []string{ + XDGRuntimeEnv, + dbusEnv, + }, + "wpctl", + "status", + ) + + Poll(50*time.Millisecond, cred, []string{ + XDGRuntimeEnv, + dbusEnv, + }, + "pw-cli", + "load-module", + "libpipewire-module-null-sink", + ) + + _, _done := MustStartWith( + context.Background(), cred, quietServices, []string{ + XDGRuntimeEnv, + dbusEnv, + }, nil, + "wireplumber", + ) + + go func() { + if _err := <-_done; _err != nil { + log.Fatalf("wireplumber terminated unexpectedly: %v", _err) + } + log.Fatal("wireplumber terminated unexpectedly") + }() + return +} + +// MustScanFor continuously scans the proc filesystem and calls f for each entry +// visited. +func MustScanFor(f func(ps *StatScanner) bool) int { + var ps StatScanner + + for ps.Scan() { + if f(&ps) { + break + } + } + if err := ps.Err(); err != nil { + log.Fatal(err) + } + return ps.Stat().PID +} + +// storeLookup describes a store lookup request. +type storeLookup struct { + // Identifier to look up. + id hst.ID + // Address of value to write result to. + p *hst.State + // Error encountered while fulfilling the request. + done chan<- error +} + +var ( + // storeThreads holds channels for dispatching requests to goroutines + // locked to threads configured with specific credentials. + storeThreads = make(map[[2]uint32]chan<- storeLookup) + // storeThreadsMu synchronises access to storeThreads. + storeThreadsMu sync.Mutex +) + +// LookupStore looks up the specified identifier from the hakurei state store +// and stores its contents in the value pointed to by p. +func LookupStore(cred *syscall.Credential, id hst.ID, p *hst.State) error { + var key [2]uint32 + if cred != nil { + key[0], key[1] = cred.Uid, cred.Gid + } + + storeThreadsMu.Lock() + c, ok := storeThreads[key] + if !ok { + s := store.New(check.MustAbs(HakureiPrefix + "state")) + r := make(chan storeLookup) + go func() { + if cred != nil { + runtime.LockOSThread() + if err := syscall.Setfsuid(int(cred.Uid)); err != nil { + log.Fatalf("setfsuid: %v", err) + } else if err = syscall.Setfsgid(int(cred.Gid)); err != nil { + log.Fatalf("setfsgid: %v", err) + } + } + + req: + for req := range r { + ok = false + entries, copyError := s.All() + for entry := range entries { + if entry.ID == req.id { + ok = true + if _, err := entry.Load(req.p, nil); err != nil { + req.done <- err + continue req + } + break + } + } + if err := copyError(); err != nil { + req.done <- err + continue + } + if !ok { + req.done <- fmt.Errorf("instance %s is not present in store", id.String()) + continue + } + req.done <- nil + } + }() + c = r + storeThreads[key] = c + } + storeThreadsMu.Unlock() + + done := make(chan error, 1) + c <- storeLookup{id, p, done} + return <-done +} + +// StartHakurei starts a hakurei container configured by c. +func StartHakurei( + ctx context.Context, + wg *sync.WaitGroup, + cred *syscall.Credential, + dbusEnv string, + processCommand func(cmd *exec.Cmd), + onDone func(err error, output string), + scan, wait, verbose, insecure bool, + c *hst.Config, +) (p hst.State, pid int, err error) { + var hakureiFlags string + if verbose { + hakureiFlags += " -v" + } + if insecure { + hakureiFlags += " --insecure" + } + + var notifySuffix string + if wait { + notifySuffix += " 1>&3" + } + + cmd := exec.CommandContext(ctx, + "script", "/dev/null", + "-E", "always", + "-qec", + "hakurei"+hakureiFlags+" run "+ + "--identifier-fd=5"+ + " 4"+notifySuffix, + ) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGTERM, + Credential: cred, + } + var output strings.Builder + cmd.Stdout, cmd.Stderr = &output, &output + cmd.Env = SessionEnv(dbusEnv) + + var notify, _notify, _conf, conf, ident, _ident *os.File + if notify, _notify, err = os.Pipe(); err != nil { + return + } + cmd.ExtraFiles = append(cmd.ExtraFiles, _notify) + if _conf, conf, err = os.Pipe(); err != nil { + _, _ = notify.Close(), _notify.Close() + return + } + cmd.ExtraFiles = append(cmd.ExtraFiles, _conf) + if ident, _ident, err = os.Pipe(); err != nil { + _, _ = notify.Close(), _notify.Close() + _, _ = conf.Close(), _conf.Close() + return + } + cmd.ExtraFiles = append(cmd.ExtraFiles, _ident) + if processCommand != nil { + processCommand(cmd) + } + + done := MustStart(cmd) + wg.Go(func() { _err := <-done; onDone(_err, output.String()) }) + + if err = errors.Join( + _notify.Close(), + _conf.Close(), + _ident.Close(), + ); err != nil { + _, _, _ = notify.Close(), conf.Close(), ident.Close() + return + } + + if err = json.NewEncoder(conf).Encode(c); err != nil { + _, _, _ = notify.Close(), conf.Close(), ident.Close() + return + } else if err = conf.Close(); err != nil { + _, _ = notify.Close(), ident.Close() + return + } + + if !scan && len(cmd.ExtraFiles) != 3 { + err = errors.Join(notify.Close(), ident.Close()) + return + } + + var id hst.ID + if _, err = io.ReadFull(ident, id[:]); err != nil { + _, _ = notify.Close(), ident.Close() + if !scan && errors.Is(err, io.EOF) { + err = nil + } + return + } else if err = ident.Close(); err != nil { + _ = notify.Close() + return + } + + if !scan { + err = notify.Close() + return + } + + if wait { + if _, err = io.ReadFull(notify, make([]byte, 8)); err != nil { + _ = notify.Close() + return + } + } + if err = notify.Close(); err != nil { + return + } + + var identifiers []hst.ID + MustDecodeFrom( + ctx, cred, nil, &identifiers, + "hakurei", "--json", "ps", "--short", + ) + if !slices.Contains(identifiers, id) { + log.Fatalf("(%s) instance not present in ps output", c.Container.Hostname) + } + + var got hst.State + MustDecodeFrom( + ctx, cred, nil, &got, + "hakurei", "--json", "show", id.String(), + ) + + if err = LookupStore(cred, id, &p); err != nil { + return + } + + if !reflect.DeepEqual(got, p) { + log.Fatalf("show: %#v, want %#v", got, p) + } + + var stat syscall.Stat_t + pid = MustScanFor(func(ps *StatScanner) bool { + select { + case err = <-done: + if err == nil { + log.Fatal("test process terminated unexpectedly") + } + log.Fatal(err) + default: + break + } + + var args []string + if args, err = ps.Stat().Args(); err != nil { + if IsNotExist(err) { + return false + } + log.Fatal(err) + } else if !slices.Equal(args, c.Container.Args) { + return false + } + + if err = ps.Stat().Stat(&stat); err != nil { + if IsNotExist(err) { + return false + } + log.Fatal(err) + } + + uid := hst.ToUser[uint32](0, uint32(c.Identity)) + if stat.Uid != uid || stat.Gid != uid { + return false + } + + var t []byte + if t, err = os.ReadFile(filepath.Join( + fhs.Proc, + strconv.Itoa(ps.Stat().PPID), + "stat", + )); err != nil { + if IsNotExist(err) { + return false + } + log.Fatal(err) + } + + var _stat Stat + if err = _stat.UnmarshalText(t); err != nil { + log.Fatal(err) + } + if _stat.PPID != p.ShimPID { + return false + } + + return true + }) + return +} + +// MustStartHakurei is like [StartHakurei], but terminates the test suite if +// an error is encountered. +func MustStartHakurei( + ctx context.Context, + wg *sync.WaitGroup, + cred *syscall.Credential, + dbusEnv string, + onDone func(err error, output string), + scan, wait, verbose, insecure bool, + c *hst.Config, +) (p hst.State, pid int) { + _ctx, cancel := context.WithCancel(ctx) + + var err error + p, pid, err = StartHakurei( + _ctx, wg, cred, dbusEnv, + nil, func(err error, output string) { + defer cancel() + onDone(err, output) + }, scan, wait, verbose, insecure, c, + ) + if err != nil { + log.Printf( + "(%s) cannot start hakurei container: %v", + c.Container.Hostname, + err, + ) + cancel() + } + return +} diff --git a/internal/testsuite/testsuite_guard.go b/internal/testsuite/testsuite_guard.go new file mode 100644 index 00000000..0859d17f --- /dev/null +++ b/internal/testsuite/testsuite_guard.go @@ -0,0 +1,15 @@ +//go:build !testsuite && !tester + +package testsuite + +import ( + "os" + "testing" +) + +func init() { + if !testing.Testing() { + println("package testsuite imported in non-testsuite program") + os.Exit(1) + } +} diff --git a/internal/testsuite/testsuite_root.go b/internal/testsuite/testsuite_root.go new file mode 100644 index 00000000..11f503ef --- /dev/null +++ b/internal/testsuite/testsuite_root.go @@ -0,0 +1,17 @@ +//go:build testsuite + +package testsuite + +import ( + "log" + "os" +) + +func init() { + if os.Geteuid() != 0 { + log.Fatal("this program must run as root") + } + + log.SetFlags(0) + log.SetPrefix("testsuite: ") +} diff --git a/internal/wayland/security-context-v1-protocol.c b/internal/wayland/security-context-v1-protocol.c index 5b15b1b4..54cd69d2 100644 --- a/internal/wayland/security-context-v1-protocol.c +++ b/internal/wayland/security-context-v1-protocol.c @@ -1,4 +1,4 @@ -/* Generated by wayland-scanner 1.23.1 */ +/* Generated by wayland-scanner 1.26.0 */ /* * Copyright © 2021 Simon Ser diff --git a/internal/wayland/security-context-v1-protocol.h b/internal/wayland/security-context-v1-protocol.h index f973619e..0b5bc69e 100644 --- a/internal/wayland/security-context-v1-protocol.h +++ b/internal/wayland/security-context-v1-protocol.h @@ -1,4 +1,4 @@ -/* Generated by wayland-scanner 1.23.1 */ +/* Generated by wayland-scanner 1.26.0 */ #ifndef SECURITY_CONTEXT_V1_CLIENT_PROTOCOL_H #define SECURITY_CONTEXT_V1_CLIENT_PROTOCOL_H diff --git a/internal/wayland/wayland-client-helper.c b/internal/wayland/wayland-client-helper.c index 25af847b..0af69403 100644 --- a/internal/wayland/wayland-client-helper.c +++ b/internal/wayland/wayland-client-helper.c @@ -8,110 +8,119 @@ #include "security-context-v1-protocol.h" #include <wayland-client.h> -static void registry_handle_global( - void *data, - struct wl_registry *registry, - uint32_t name, - const char *interface, - uint32_t version) { - struct wp_security_context_manager_v1 **out = data; +static void registry_handle_global(void *data, struct wl_registry *registry, + uint32_t name, const char *interface, + uint32_t version) +{ + struct wp_security_context_manager_v1 **out = data; - if (strcmp(interface, wp_security_context_manager_v1_interface.name) == 0) - *out = wl_registry_bind(registry, name, &wp_security_context_manager_v1_interface, 1); + if (strcmp(interface, wp_security_context_manager_v1_interface.name) == + 0) + *out = wl_registry_bind( + registry, name, + &wp_security_context_manager_v1_interface, 1); } -static void registry_handle_global_remove( - void *data, - struct wl_registry *registry, - uint32_t name) {} /* no-op */ +static void registry_handle_global_remove(void *data, + struct wl_registry *registry, + uint32_t name) +{ +} /* no-op */ static const struct wl_registry_listener registry_listener = { - .global = registry_handle_global, - .global_remove = registry_handle_global_remove, + .global = registry_handle_global, + .global_remove = registry_handle_global_remove, }; -hakurei_wayland_res hakurei_security_context_bind( - char *socket_path, - int server_fd, - const char *app_id, - const char *instance_id, - int close_fd) { - hakurei_wayland_res res = HAKUREI_WAYLAND_SUCCESS; /* see wayland.go for handling */ +hakurei_wayland_res hakurei_security_context_bind(char *socket_path, + int server_fd, + const char *app_id, + const char *instance_id, + int close_fd) +{ + hakurei_wayland_res res = + HAKUREI_WAYLAND_SUCCESS; /* see wayland.go for handling */ - struct wl_display *display = NULL; - struct wl_registry *registry; - struct wp_security_context_manager_v1 *security_context_manager = NULL; - int event_cnt; - int listen_fd = -1; - struct sockaddr_un sockaddr = {0}; - struct wp_security_context_v1 *security_context; + struct wl_display *display = NULL; + struct wl_registry *registry; + struct wp_security_context_manager_v1 *security_context_manager = NULL; + int event_cnt; + int listen_fd = -1; + struct sockaddr_un sockaddr = { 0 }; + struct wp_security_context_v1 *security_context; - display = wl_display_connect_to_fd(server_fd); - if (display == NULL) { - res = HAKUREI_WAYLAND_CONNECT; - goto out; - }; + display = wl_display_connect_to_fd(server_fd); + if (display == NULL) { + res = HAKUREI_WAYLAND_CONNECT; + goto out; + }; - registry = wl_display_get_registry(display); - if (wl_registry_add_listener(registry, ®istry_listener, &security_context_manager) < 0) { - res = HAKUREI_WAYLAND_LISTENER; - goto out; - } - event_cnt = wl_display_roundtrip(display); - wl_registry_destroy(registry); - if (event_cnt < 0) { - res = HAKUREI_WAYLAND_ROUNDTRIP; - goto out; - } + registry = wl_display_get_registry(display); + if (wl_registry_add_listener(registry, ®istry_listener, + &security_context_manager) < 0) { + res = HAKUREI_WAYLAND_LISTENER; + goto out; + } + event_cnt = wl_display_roundtrip(display); + wl_registry_destroy(registry); + if (event_cnt < 0) { + res = HAKUREI_WAYLAND_ROUNDTRIP; + goto out; + } - if (security_context_manager == NULL) { - res = HAKUREI_WAYLAND_NOT_AVAIL; - goto out; - } + if (security_context_manager == NULL) { + res = HAKUREI_WAYLAND_NOT_AVAIL; + goto out; + } - listen_fd = socket(AF_UNIX, SOCK_STREAM, 0); - if (listen_fd < 0) { - res = HAKUREI_WAYLAND_SOCKET; - goto out; - } + listen_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (listen_fd < 0) { + res = HAKUREI_WAYLAND_SOCKET; + goto out; + } - sockaddr.sun_family = AF_UNIX; - snprintf(sockaddr.sun_path, sizeof(sockaddr.sun_path), "%s", socket_path); - if (bind(listen_fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) != 0) { - res = HAKUREI_WAYLAND_BIND; - goto out; - } + sockaddr.sun_family = AF_UNIX; + snprintf(sockaddr.sun_path, sizeof(sockaddr.sun_path), "%s", + socket_path); + if (bind(listen_fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) != + 0) { + res = HAKUREI_WAYLAND_BIND; + goto out; + } - if (listen(listen_fd, 0) != 0) { - res = HAKUREI_WAYLAND_LISTEN; - goto out; - } + if (listen(listen_fd, 0) != 0) { + res = HAKUREI_WAYLAND_LISTEN; + goto out; + } - security_context = wp_security_context_manager_v1_create_listener(security_context_manager, listen_fd, close_fd); - if (security_context == NULL) { /* not reached */ - res = HAKUREI_WAYLAND_NOT_AVAIL; - goto out; - } - wp_security_context_v1_set_sandbox_engine(security_context, "app.hakurei"); - wp_security_context_v1_set_app_id(security_context, app_id); - wp_security_context_v1_set_instance_id(security_context, instance_id); - wp_security_context_v1_commit(security_context); - wp_security_context_v1_destroy(security_context); - if (wl_display_roundtrip(display) < 0) { - res = HAKUREI_WAYLAND_ROUNDTRIP; - goto out; - } + security_context = wp_security_context_manager_v1_create_listener( + security_context_manager, listen_fd, close_fd); + if (security_context == NULL) { /* not reached */ + res = HAKUREI_WAYLAND_NOT_AVAIL; + goto out; + } + wp_security_context_v1_set_sandbox_engine(security_context, + "app.hakurei"); + wp_security_context_v1_set_app_id(security_context, app_id); + wp_security_context_v1_set_instance_id(security_context, instance_id); + wp_security_context_v1_commit(security_context); + wp_security_context_v1_destroy(security_context); + if (wl_display_roundtrip(display) < 0) { + res = HAKUREI_WAYLAND_ROUNDTRIP; + goto out; + } out: - if (listen_fd >= 0) - close(listen_fd); - if (security_context_manager != NULL) - wp_security_context_manager_v1_destroy(security_context_manager); - if (display != NULL) - wl_display_disconnect(display); + if (listen_fd >= 0) + close(listen_fd); + if (security_context_manager != NULL) + wp_security_context_manager_v1_destroy( + security_context_manager); + if (display != NULL) + wl_display_disconnect(display); - free((void *)socket_path); - free((void *)app_id); - free((void *)instance_id); - return res; + free((void *)socket_path); + free((void *)app_id); + free((void *)instance_id); + return res; } diff --git a/internal/wayland/wayland-client-helper.h b/internal/wayland/wayland-client-helper.h index ce67f6f6..09348be4 100644 --- a/internal/wayland/wayland-client-helper.h +++ b/internal/wayland/wayland-client-helper.h @@ -2,41 +2,41 @@ #include <sys/un.h> typedef enum { - HAKUREI_WAYLAND_SUCCESS, - /* wl_display_connect_to_fd failed, errno */ - HAKUREI_WAYLAND_CONNECT, - /* wl_registry_add_listener failed, errno */ - HAKUREI_WAYLAND_LISTENER, - /* wl_display_roundtrip failed, errno */ - HAKUREI_WAYLAND_ROUNDTRIP, - /* compositor does not implement wp_security_context_v1 */ - HAKUREI_WAYLAND_NOT_AVAIL, - /* socket failed, errno */ - HAKUREI_WAYLAND_SOCKET, - /* bind failed, errno */ - HAKUREI_WAYLAND_BIND, - /* listen failed, errno */ - HAKUREI_WAYLAND_LISTEN, + HAKUREI_WAYLAND_SUCCESS, + /* wl_display_connect_to_fd failed, errno */ + HAKUREI_WAYLAND_CONNECT, + /* wl_registry_add_listener failed, errno */ + HAKUREI_WAYLAND_LISTENER, + /* wl_display_roundtrip failed, errno */ + HAKUREI_WAYLAND_ROUNDTRIP, + /* compositor does not implement wp_security_context_v1 */ + HAKUREI_WAYLAND_NOT_AVAIL, + /* socket failed, errno */ + HAKUREI_WAYLAND_SOCKET, + /* bind failed, errno */ + HAKUREI_WAYLAND_BIND, + /* listen failed, errno */ + HAKUREI_WAYLAND_LISTEN, - /* ensure pathname failed, implemented in conn.go */ - HAKUREI_WAYLAND_CREAT, - /* socket for host server failed, implemented in conn.go */ - HAKUREI_WAYLAND_HOST_SOCKET, - /* connect for host server failed, implemented in conn.go */ - HAKUREI_WAYLAND_HOST_CONNECT, - /* cleanup failed, implemented in conn.go */ - HAKUREI_WAYLAND_CLEANUP, + /* ensure pathname failed, implemented in conn.go */ + HAKUREI_WAYLAND_CREAT, + /* socket for host server failed, implemented in conn.go */ + HAKUREI_WAYLAND_HOST_SOCKET, + /* connect for host server failed, implemented in conn.go */ + HAKUREI_WAYLAND_HOST_CONNECT, + /* cleanup failed, implemented in conn.go */ + HAKUREI_WAYLAND_CLEANUP, } hakurei_wayland_res; -hakurei_wayland_res hakurei_security_context_bind( - char *socket_path, - int server_fd, - const char *app_id, - const char *instance_id, - int close_fd); +hakurei_wayland_res hakurei_security_context_bind(char *socket_path, + int server_fd, + const char *app_id, + const char *instance_id, + int close_fd); /* returns whether the specified size fits in the sun_path field of sockaddr_un */ -static inline bool hakurei_is_valid_size_sun_path(size_t sz) { - struct sockaddr_un sockaddr; - return sz <= sizeof(sockaddr.sun_path); +static inline bool hakurei_is_valid_size_sun_path(size_t sz) +{ + struct sockaddr_un sockaddr; + return sz <= sizeof(sockaddr.sun_path); }; diff --git a/internal/workflows/Containerfile b/internal/workflows/Containerfile new file mode 100644 index 00000000..82ce8820 --- /dev/null +++ b/internal/workflows/Containerfile @@ -0,0 +1,93 @@ +FROM docker.gitea.com/runner-images:ubuntu-latest + +RUN rm /var/run && ln -sf ../run /var && mkdir /var/empty +RUN add-apt-repository -y ppa:savoury1/pipewire && \ + apt-get update && apt-get install -y \ + msft-golang \ + libmount-dev \ + acl \ + sway \ + xwayland \ + x11-xserver-utils \ + xdg-dbus-proxy \ + libnotify-bin \ + pipewire \ + pipewire-pulse \ + wireplumber- \ + pulseaudio-utils \ + foot \ + xterm \ + grim \ + fuse3 \ + fsmark && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +RUN PACKAGES=" \ + build-essential \ + ninja-build \ + bison \ + libxcb-composite0-dev \ + libxcb-ewmh-dev \ + libxcb-icccm4-dev \ + libxcb-res0-dev \ + libjson-c-dev \ + libpango1.0-dev \ + libevdev-dev \ + libudev-dev \ + libpipewire-0.3-dev" && \ + apt-get update && apt-get install -y $PACKAGES && \ + VERSION="1.12.1" && \ + curl -L "https://github.com/mesonbuild/meson/releases/download/$VERSION/meson-$VERSION.tar.gz" | \ + tar -C /tmp -zx && \ + MESON_DIR="/tmp/meson-$VERSION" && \ + MESON="$MESON_DIR/meson.py" && \ + VERSION="0.20.2" && \ + curl "https://gitlab.freedesktop.org/wlroots/wlroots/-/archive/$VERSION/wlroots-${VERSION}.tar.bz2?ref_type=tags" | \ + tar -C /tmp -jx && \ + "$MESON" setup \ + --wrap-mode=default \ + -Dprefix=/usr \ + -Dbuildtype=release \ + -Dc_args='-w' \ + -Dxwayland=enabled \ + "/tmp/wlroots-$VERSION" build && \ + "$MESON" compile -C build && \ + "$MESON" install -C build && \ + rm -rf build "/tmp/wlroots-$VERSION" && \ + VERSION="1.32.0" && \ + curl "https://gitlab.freedesktop.org/libinput/libinput/-/archive/$VERSION/libinput-$VERSION.tar.bz2?ref_type=tags" | \ + tar -C /tmp -jx && \ + "$MESON" setup \ + -Dprefix=/usr \ + -Dbuildtype=release \ + -Dmtdev=false \ + -Dlibwacom=false \ + -Ddebug-gui=false \ + "/tmp/libinput-$VERSION" build && \ + "$MESON" compile -C build && \ + "$MESON" install -C build && \ + rm -rf build "/tmp/libinput-$VERSION" && \ + VERSION="1.12" && \ + curl -L "https://github.com/swaywm/sway/releases/download/$VERSION/sway-$VERSION.tar.gz" | \ + tar -C /tmp -zx && \ + "$MESON" setup \ + -Dprefix=/usr \ + -Dbuildtype=release \ + "/tmp/sway-$VERSION" build && \ + "$MESON" compile -C build && \ + "$MESON" install -C build && \ + rm -rf build "/tmp/sway-$VERSION" && \ + VERSION='0.5.14' && \ + curl "https://gitlab.freedesktop.org/pipewire/wireplumber/-/archive/${VERSION}/wireplumber-${VERSION}.tar.bz2" | \ + tar -C /tmp -jx && \ + "$MESON" setup \ + -Dprefix=/usr \ + -Dbuildtype=release \ + "/tmp/wireplumber-${VERSION}" build && \ + "$MESON" compile -C build && \ + "$MESON" install -C build && \ + rm -rf build "/tmp/wireplumber-${VERSION}" && \ + apt-get remove -y $PACKAGES && \ + apt-get autoremove -y && \ + apt-get clean && rm -rf /var/lib/apt/lists/* && \ + rm -rf "$MESON_DIR" diff --git a/internal/workflows/build.sh b/internal/workflows/build.sh new file mode 100755 index 00000000..2df8656e --- /dev/null +++ b/internal/workflows/build.sh @@ -0,0 +1,5 @@ +#!/bin/sh -e + +TAG="git.gensokyo.uk/rosa/runner:latest" +podman build -t "$TAG" --format docker "$(dirname -- "$0")" +podman push "$TAG" diff --git a/internal/workflows/doc.go b/internal/workflows/doc.go new file mode 100644 index 00000000..e220ed67 --- /dev/null +++ b/internal/workflows/doc.go @@ -0,0 +1,162 @@ +//go:build !workflows + +/* +The workflows program manages the .gitea directory to avoid having to write +yaml. It is never invoked directly or built for the distribution tarball. + +This program is an internal detail of the hakurei project and is not usable +on its own. It is not covered by the compatibility promise. + +# Configuring act_runner + +It is unreasonable and impractical to bootstrap Rosa OS each time a job runs. +This section documents an ideal configuration for exposing the cmd/mbf CI +service to all job containers, while avoiding some issues with the container +setup documented upstream. + +Microsoft Github workflows expect a docker socket in the job container. This is +problematic because docker has a hard time running inside a docker container. +The Gitea act_runner simply bind mounts whatever socket it sees into the +container. With a regular docker daemon, this allows not only a simple container +escape, but also privilege escalation as unconstrained root in the init +namespace. To mitigate this, set up an unprivileged podman daemon and expose its +socket to the container instead. Since mountinfo always use credentials from the +init user namespace, subordinate user and group ID must always be 100000. + +On Alpine Linux, this is achieved by: + + # Configuration for /etc/init.d/podman + + # See podman-system-service(1) for service description + # and available options. + #podman_opts="--time 0" + + # API endpoint in URI form. Leave empty to use defaults. + podman_uri="unix:///srv/act_runner/podman" + + # Setting root user will start rootful service. + # Use any other user for rootless mode. + podman_user="act_runner" + + # Required by cmd/hakurei test suite. + rc_ulimit="-r 49" + +A symlink must then be created at /var/run/docker.sock pointing to the socket +owned by the unprivileged daemon. This should be done in a /etc/local.d script. + +The container can be specified via docker-compose: + + version: "3.8" + services: + runner: + restart: always + image: docker.io/gitea/act_runner:nightly + networks: + - act_runner + environment: + CONFIG_FILE: /config.yaml + GITEA_INSTANCE_URL: "https://git.gensokyo.uk" + + GITEA_RUNNER_REGISTRATION_TOKEN: "${REGISTRATION_TOKEN}" + GITEA_RUNNER_NAME: "${RUNNER_NAME}" + volumes: + - ./config.yaml:/config.yaml + - ./data:/data + - ./podman:/var/run/docker.sock + + networks: + act_runner: + name: act_runner + +Before starting the container, configure act_runner via config.yaml: + + container: + network: act_runner + options: -e PAGER=cat -e MBF_CACHE_DIR=/rosa -e MBF_POISON_OPEN=1 + -v /var/lib/rosa:/rosa + --security-opt='unmask=/proc/*' + --cap-add=SYS_ADMIN + --cap-add=SYS_PTRACE + --device=/dev/kvm + --device=/dev/fuse + valid_volumes: + - /var/lib/rosa + + runner: + labels: + - 'rosa:docker://git.gensokyo.uk/rosa/runner:latest' + +where /var/lib/rosa is the absolute pathname of the cache directory in the init +namespace. Setting MBF_POISON_OPEN enables cmd/mbf to run as root. It is also +a good idea here to set runner.capacity to reflect the capacity of the guest, so +jobs can be consumed quicker. Removing mount points covering /proc enables +testing of cmd/hakurei. Exposing the fuse device and adding capability +CAP_SYS_ADMIN enables testing of cmd/sharefs. Adding capability CAP_SYS_PTRACE +enables dumping seccomp filters via ptrace on the patched kernel. + +Build a statically-linked cmd/mbf: + + go build -trimpath \ + -ldflags='-s -w + -buildid= + -linkmode external + -extldflags=-static' \ + ./cmd/mbf + +Create a directory named "bin" in the cache directory and install the resulting +binary. + +The cmd/mbf CI service should generally run as a separate user. To achieve this, +change the socket permission to 0777 after the service starts. On Alpine Linux, +this can be achieved by the init script: + + #!/sbin/openrc-run + supervisor=supervise-daemon + + name="Rosa OS cmd/mbf CI backend" + description="Listening service that services CI workloads from act_runner" + + command=/var/lib/rosa/bin/mbf + command_args="-d /var/lib/rosa ci daemon" + command_user="${mbf_user:=mbf}" + + output_logger="logger" + error_logger="logger" + + depend() { + need sysfs + } + + start_post() { + while [ ! -e /var/lib/rosa/ci ]; do sleep 0.1; done + chmod 0777 /var/lib/rosa/ci + } + +It is often a good idea to populate the cache from a mirror service before the +first workflow job is started and re-populate it after every cmd/mbf update. + +# Configuring the kernel + +In order to attach to the container process, the sysctl kernel.yama.ptrace_scope +must be set to 0. After which, apply the patch internal/workflows/seccomp.patch +to your kernel sources, compile and install the new kernel. Refer to +https://wiki.alpinelinux.org/wiki/Custom_Kernel if the guest runs Alpine Linux. +If running podman or docker as root, the patch is not required. Do not apply +this patch on a system meant to be secure. + +# Security + +The design of Microsoft Github workflows is inherently insecure: it requires +internet access just to fetch actions, and these actions all expect internet +access as well. Gitea, being a drop-in replacement, retains the same problems. + +The setup documented above already avoids the obvious container escape via the +bind-mounted docker socket, but it is still easily possible to escape as the +context of the unconstrained runner user. For this reason, the runner must +reside in a hardened guest. Since internet access cannot be disabled, extra care +must be taken when setting up the host firewall to prevent access to internal +systems from the untrusted guest. +*/ +package workflows + +//go:generate go run -tags=workflows . diff --git a/internal/workflows/main.go b/internal/workflows/main.go new file mode 100644 index 00000000..81b14a12 --- /dev/null +++ b/internal/workflows/main.go @@ -0,0 +1,151 @@ +//go:build workflows + +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" + "sync" +) + +// KV represents a string to value pair. +type KV[T any] struct { + K string + V T +} + +// A Map represents an ordered set of key-value pairs. +type Map[T any] []KV[T] + +// MarshalJSON encodes m as a key-value pair in JSON. +func (m Map[T]) MarshalJSON() ([]byte, error) { + if m == nil || len(m) == 0 { + return []byte("{}"), nil + } + + buf := bytes.NewBuffer(make([]byte, 0, len(m)<<5)) + buf.WriteByte('{') + + last := len(m) - 1 + for i, pair := range m { + p, err := json.Marshal(pair.K) + if err != nil { + return nil, fmt.Errorf("key %q: %w", pair.K, err) + } + buf.Write(p) + buf.WriteByte(':') + + p, err = json.Marshal(pair.V) + if err != nil { + return nil, fmt.Errorf("value of key %q: %w", pair.K, err) + } + buf.Write(p) + + if i < last { + buf.WriteByte(',') + } + } + + buf.WriteByte('}') + return buf.Bytes(), nil +} + +// A Step is a task ran as part of a [Job]. +type Step struct { + // A name for the step. + Name string `json:"name,omitempty"` + // Selects an action to run as part of a step in the job. + Uses string `json:"uses,omitempty"` + // A unique identifier for the step. + ID string `json:"id,omitempty"` + // Prevent a step from running unless a condition is met. + If string `json:"if,omitempty"` + // Runs command-line programs that do not exceed 21,000 characters using the + // operating system's shell. + Run string `json:"run,omitempty"` + // Key-value pairs of input parameters defined by the action. + With Map[any] `json:"with,omitempty"` +} + +// A Job is an entry in a [Workflow] document. +type Job struct { + // Set a name for the job. + Name string `json:"name,omitempty"` + // Identifies any jobs that must complete successfully before this job will + // run. + Needs []string `json:"needs,omitempty"` + // Define the type of machine to run the job on. + On string `json:"runs-on,omitempty"` + // Create a map of outputs for a job. + Outputs Map[string] `json:"outputs,omitempty"` + // A sequence of tasks. + Steps []Step `json:"steps,omitempty"` +} + +// Workflow describes a workflow document. +type Workflow struct { + // The name of the workflow. + Name string `json:"name,omitempty"` + // The name for workflow runs generated from the workflow. + RunName string `json:"run-name,omitempty"` + // Define which events can cause the workflow to run. + On any `json:"on"` + + // Named jobs part of the workflow run. + Jobs Map[Job] `json:"jobs,omitempty"` +} + +// workflowsDir is the relative pathname of the workflows directory. +const workflowsDir = "../../.gitea/workflows" + +// writeFile writes the action to the .gitea directory. +func (w *Workflow) writeFile(name string) error { + f, err := os.OpenFile( + filepath.Join(workflowsDir, name+".yml"), + os.O_CREATE|os.O_EXCL|os.O_WRONLY, + 0444, + ) + if err != nil { + return err + } + + err = json.NewEncoder(f).Encode(w) + if closeErr := f.Close(); err == nil { + err = closeErr + } + return err +} + +// removeOnce synchronises removal of stale workflow files. +var removeOnce sync.Once + +// removeWorkflows removes existing workflow files once. +func removeWorkflows() { + removeOnce.Do(func() { + if err := os.RemoveAll(workflowsDir); err != nil { + fmt.Println(err) + os.Exit(1) + } + if err := os.Mkdir(workflowsDir, 0755); err != nil { + fmt.Println(err) + os.Exit(1) + } + }) +} + +// mustWriteFile is like writeFile, but terminates the program if a non-nil +// error is returned. +func (w *Workflow) mustWriteFile(name string) (_ struct{}) { + removeWorkflows() + + if err := w.writeFile(name); err != nil { + fmt.Println(err) + os.Exit(1) + } + return +} + +func main() {} diff --git a/internal/workflows/release.go b/internal/workflows/release.go new file mode 100644 index 00000000..a2f4c687 --- /dev/null +++ b/internal/workflows/release.go @@ -0,0 +1,50 @@ +//go:build workflows + +package main + +var _ = (&Workflow{ + Name: "Release", + On: Map[any]{{"push", Map[[]string]{{"tags", []string{"v*"}}}}}, + + Jobs: []KV[Job]{ + {"release", Job{ + Name: "Create release", + On: "rosa", + + Steps: []Step{ + { + Name: "Checkout", + Uses: "actions/checkout@v4", + + With: []KV[any]{ + {"fetch-depth", 0}, + {"fetch-tags", true}, + }, + }, + + { + Name: "Request distribution", + ID: "dist", + Run: "/rosa/bin/mbf ci dist -o result " + + ". \"$(cat cmd/dist/VERSION)-act\" && " + + "git tag " + + "--list " + + "--format='%(contents:body)' " + + "\"$(cat cmd/dist/VERSION)\" >> " + + "\"/tmp/release-body\"", + }, + + { + Name: "Create release", + Uses: "https://gitea.com/actions/gitea-release-action@v1.3.7", + + With: []KV[any]{ + {"files", "result/hakurei-**"}, + {"body_path", "/tmp/release-body"}, + {"draft", true}, + }, + }, + }, + }}, + }, +}).mustWriteFile("release") diff --git a/internal/workflows/seccomp.patch b/internal/workflows/seccomp.patch new file mode 100644 index 00000000..ddabc71e --- /dev/null +++ b/internal/workflows/seccomp.patch @@ -0,0 +1,18 @@ +diff --git a/kernel/seccomp.c b/kernel/seccomp.c +index 25f62867a16d..7b63ccc8daf4 100644 +--- a/kernel/seccomp.c ++++ b/kernel/seccomp.c +@@ -2216,8 +2216,12 @@ long seccomp_get_filter(struct task_struct *task, unsigned long filter_off, + struct seccomp_filter *filter; + struct sock_fprog_kern *fprog; + long ret; ++ struct user_namespace *user_ns = current_user_ns(); + +- if (!capable(CAP_SYS_ADMIN) || ++ if (in_userns(user_ns, task_cred_xxx(task, user_ns))) { ++ if (!ns_capable(user_ns, CAP_SYS_ADMIN)) ++ return -EACCES; ++ } else if (!capable(CAP_SYS_ADMIN) || + current->seccomp.mode != SECCOMP_MODE_DISABLED) { + return -EACCES; + } diff --git a/internal/workflows/step.go b/internal/workflows/step.go new file mode 100644 index 00000000..3458c0ea --- /dev/null +++ b/internal/workflows/step.go @@ -0,0 +1,60 @@ +//go:build workflows + +package main + +// checkout is the standard actions/checkout step. +var checkout = Step{Name: "Checkout", Uses: "actions/checkout@v4"} + +// newUploadArtifact returns an actions/upload-artifact step uploading +// everything in the result directory as the specified name. +func newUploadArtifact(display, name string) Step { + return Step{ + Name: "Upload " + display, + Uses: "actions/upload-artifact@v3", + + With: Map[any]{ + {"name", name}, + {"path", "result/*"}, + + {"retention-days", 1}, + }, + } +} + +// newCIRequest returns a step for requesting a mbf CI task. +func newCIRequest(display, name, id string) Step { + return Step{ + Name: "Request " + display, + ID: id, + Run: "HAKUREI_REV=\"$(git rev-parse --short HEAD)\" && " + + "/rosa/bin/mbf ci " + name + " " + + ". \"$(cat cmd/dist/VERSION)-$HAKUREI_REV\" && " + + "echo \"rev=$HAKUREI_REV\" >> \"$GITHUB_OUTPUT\"", + } +} + +var ( + // dist is a step to obtain a hakurei distribution. + dist = newCIRequest("distribution", "dist -o /tmp/dist", "dist") + // _dist is like dist, but with race instrument. + _dist = newCIRequest("distribution", "race -o /tmp/dist", "dist") + + // install is a step to unpack and deploy a hakurei distribution in /tmp/dist. + install = Step{ + Name: "Install hakurei", + Run: "HAKUREI_VERSION=\"$(cat cmd/dist/VERSION)\" && " + + "tar -C /tmp/dist " + + "-xf /tmp/dist/hakurei-$HAKUREI_VERSION*-amd64.tar.gz && " + + "/tmp/dist/hakurei-$HAKUREI_VERSION*-amd64/install.sh && " + + "rm -rf /tmp/dist && " + + "sudo -u ubuntu hakurei version", + } +) + +// newTestsuite returns a step for running an integration test suite. +func newTestsuite(name, prefix string) Step { + return Step{ + Name: "Compile and run test suite", + Run: prefix + "go run -tags=testsuite ./cmd/" + name, + } +} diff --git a/internal/workflows/test.go b/internal/workflows/test.go new file mode 100644 index 00000000..b0819916 --- /dev/null +++ b/internal/workflows/test.go @@ -0,0 +1,86 @@ +//go:build workflows + +package main + +var _ = (&Workflow{ + Name: "Test", + On: []any{"push"}, + + Jobs: Map[Job]{ + {"hakurei", Job{ + Name: "Hakurei", + On: "rosa", + + Steps: []Step{ + checkout, + dist, + install, + newTestsuite("hakurei/testsuite", "unshare -n "), + }, + }}, + + {"hakurei-race", Job{ + Name: "Hakurei (with race instrument)", + On: "rosa", + + Steps: []Step{ + checkout, + _dist, + install, + newTestsuite("hakurei/testsuite", "unshare -n "), + }, + }}, + + {"sandbox", Job{ + Name: "Sandbox", + On: "rosa", + + Steps: []Step{ + checkout, + dist, + install, + newTestsuite("hakurei/testsuite/sandbox", "unshare -n "), + }, + }}, + + {"sandbox-race", Job{ + Name: "Sandbox (with race instrument)", + On: "rosa", + + Steps: []Step{ + checkout, + _dist, + install, + newTestsuite("hakurei/testsuite/sandbox", "unshare -n "), + }, + }}, + + {"sharefs", Job{ + Name: "ShareFS", + On: "rosa", + + Steps: []Step{ + checkout, + dist, + install, + + { + Name: "Mount sharefs", + Run: "useradd " + + "-ru 1023 " + + "-md /var/lib/sdcard " + + "-k /var/empty " + + "-s /sbin/nologin " + + "media_rw && install -dm0 /sdcard && " + + "sharefs " + + "-o rw,noexec,nosuid,nodev,noatime," + + "allow_other,mkdir,source=/var/lib/sdcard," + + "setuid=1023,setgid=1023 /sdcard", + }, + + newTestsuite("sharefs/testsuite", "sharefs -V && "), + newUploadArtifact("test output", "fs_mark"), + }, + }}, + }, +}).mustWriteFile("test") diff --git a/pkg/archive.go b/pkg/archive.go index ea4017d9..ba7c61b9 100644 --- a/pkg/archive.go +++ b/pkg/archive.go @@ -311,31 +311,28 @@ func (a archiveArtifact) Inputs() []Artifact { // IsExclusive returns false: [Reader] is fully sequential. func (archiveArtifact) IsExclusive() bool { return false } -// Cure cures the [Artifact], producing a directory located at work. -func (a archiveArtifact) Cure(t *TContext) (err error) { - var r io.ReadCloser - if r, err = t.Open(a.f); err != nil { - return - } - - defer func() { - closeErr := r.Close() - if err == nil { - err = closeErr - } - }() +// Revision satisfies [RevisionArtifact] for status behaviour. +func (archiveArtifact) Revision() uint64 { return 0 } +// Extract reads an archive stream from r and writes its contents to dir. If +// writeStatus is non-nil, it is called with a user-facing message after +// each successfully extracted entry. +func Extract( + r io.Reader, + dir string, + writeStatus func(s string) error, +) (err error) { type dirTargetPerm struct { path string mode fs.FileMode } var madeDirectories []dirTargetPerm - if err = os.MkdirAll(t.GetWorkDir().String(), 0700); err != nil { + if err = os.MkdirAll(dir, 0700); err != nil { return } var root *os.Root - if root, err = os.OpenRoot(t.GetWorkDir().String()); err != nil { + if root, err = os.OpenRoot(dir); err != nil { return } defer func() { @@ -348,6 +345,7 @@ func (a archiveArtifact) Cure(t *TContext) (err error) { var header *ArchiveHeader ar := NewReader(r) for header, err = ar.Next(); err == nil; header, err = ar.Next() { + var m string if header.Mode.IsRegular() { var f *os.File if f, err = root.OpenFile( @@ -363,18 +361,33 @@ func (a archiveArtifact) Cure(t *TContext) (err error) { } else if err = f.Close(); err != nil { return } + + m = fmt.Sprintf( + "%s %s (%d bytes)", + header.Mode, + header.Path, + header.Size, + ) } else if header.Mode&fs.ModeSymlink != 0 { var p []byte if p, err = io.ReadAll(ar); err != nil { return } + linkname := unsafe.String(unsafe.SliceData(p), len(p)) if err = root.Symlink( - unsafe.String(unsafe.SliceData(p), len(p)), + linkname, header.Path, ); err != nil { return } + + m = fmt.Sprintf( + "%s %s linkname %s", + header.Mode, + header.Path, + linkname, + ) } else if header.Mode.IsDir() { if header.Path == "." { continue @@ -387,9 +400,21 @@ func (a archiveArtifact) Cure(t *TContext) (err error) { if err = root.Mkdir(header.Path, 0700); err != nil { return } + + m = fmt.Sprintf( + "%s %s", + header.Mode, + header.Path, + ) } else { return InvalidFileModeError(header.Mode) } + + if writeStatus != nil && m != "" { + if err = writeStatus(m); err != nil { + return + } + } } if errors.Is(err, io.EOF) { err = nil @@ -406,6 +431,34 @@ func (a archiveArtifact) Cure(t *TContext) (err error) { return } +// Cure cures the [Artifact], producing a directory located at work. +func (a archiveArtifact) Cure(t *TContext) (err error) { + var status io.Writer + if status, err = t.GetStatusWriter(); err != nil { + return + } + + var r io.ReadCloser + if r, err = t.Open(a.f); err != nil { + return + } + + msg := t.GetMessage() + err = Extract(r, t.GetWorkDir().String(), func(s string) (err error) { + msg.Verbose(s) + if _, err = io.WriteString(status, s); err != nil { + return + } + _, err = status.Write([]byte{'\n'}) + return + }) + + if closeErr := r.Close(); err == nil { + err = closeErr + } + return +} + // CuresExempt exempts the cheap [KindArchive] implementation often found at // the end of a [FileArtifact] pipeline. func (archiveArtifact) CuresExempt() {} diff --git a/pkg/archive_test.go b/pkg/archive_test.go index 44ea56a7..0171d914 100644 --- a/pkg/archive_test.go +++ b/pkg/archive_test.go @@ -2,9 +2,12 @@ package pkg_test import ( "bytes" + "crypto/sha512" + "errors" "io" "io/fs" "maps" + "net/http" "reflect" "testing" "testing/fstest" @@ -176,33 +179,63 @@ func TestArchiveArtifact(t *testing.T) { want := maps.Clone(archiveTestdata) want["."].Mode = fs.ModeDir | 0500 + var buf bytes.Buffer + if err := pkg.Write(archiveTestdata, ".", &buf); err != nil { + t.Fatal(err) + } + wantChecksum := sha512.Sum384(buf.Bytes()) + wantChecksumEncode := pkg.Encode(wantChecksum) + + var transport http.Transport + client := http.Client{Transport: &transport} + transport.RegisterProtocol("file", http.NewFileTransportFS(fstest.MapFS{ + "testdata": {Data: buf.Bytes(), Mode: 0400}, + })) + checkWithCache(t, []cacheTestCase{ {"unpack", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { - var buf bytes.Buffer - if err := pkg.Write(archiveTestdata, ".", &buf); err != nil { - t.Fatal(err) - } - cureMany(t, c, []cureStep{ {"sample", pkg.NewArchive( pkg.NewFile("", buf.Bytes()), ), ignorePathname, expectsFS(want), pkg.WNew, nil}, + + {"close error passthrough", pkg.NewArchive( + pkg.NewHTTPGet( + &client, + "file:///testdata", + pkg.Checksum{}, + ), + ), nil, nil, pkg.WNew, errors.Join(&pkg.ChecksumMismatchError{ + Got: wantChecksum, + })}, }) + + dropFaults(t, base) }, expectsFS{ ".": {Mode: fs.ModeDir | 0700}, - "checksum": {Mode: fs.ModeDir | 0700}, - "checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F": {Mode: fs.ModeDir | 0500}, - "checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/block": {Mode: fs.ModeDir | 0700}, - "checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/block/uevent": {Mode: 0600}, - "checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices": {Mode: fs.ModeDir | 0700}, - "checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/empty": {Mode: fs.ModeDir | 0700}, - "checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/sub": {Mode: fs.ModeDir | 0700}, - "checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/sub/uevent": {Mode: 0600, Data: []byte("add")}, - "checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/uevent": {Mode: 0600, Data: []byte("add")}, + "checksum": {Mode: fs.ModeDir | 0700}, + "checksum/" + wantChecksumEncode: {Mode: fs.ModeDir | 0500}, + "checksum/" + wantChecksumEncode + "/block": {Mode: fs.ModeDir | 0700}, + "checksum/" + wantChecksumEncode + "/block/uevent": {Mode: 0600}, + "checksum/" + wantChecksumEncode + "/devices": {Mode: fs.ModeDir | 0700}, + "checksum/" + wantChecksumEncode + "/devices/empty": {Mode: fs.ModeDir | 0700}, + "checksum/" + wantChecksumEncode + "/devices/sub": {Mode: fs.ModeDir | 0700}, + "checksum/" + wantChecksumEncode + "/devices/sub/uevent": {Mode: 0600, Data: []byte("add")}, + "checksum/" + wantChecksumEncode + "/devices/uevent": {Mode: 0600, Data: []byte("add")}, "identifier": {Mode: fs.ModeDir | 0700}, - "identifier/-zXKfphyWM2Ko7VMVURaEMSuixNokFSe0xFbnwR2RtRtvMrcPGWalV0LIn45PXTY": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F")}, + "identifier/iyvWY0irMoW4AVRnG_du07d_nAEUtbHdeiaAYRdp-vf3wstM9u67IokOsPfU9eyM": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantChecksumEncode)}, + + "status": {Mode: fs.ModeDir | 0711}, + "status/iyvWY0irMoW4AVRnG_du07d_nAEUtbHdeiaAYRdp-vf3wstM9u67IokOsPfU9eyM": {Mode: 0444, Data: []byte(statusHeader + `drwx------ block +-rw------- block/uevent (0 bytes) +drwx------ devices +drwx------ devices/empty +drwx------ devices/sub +-rw------- devices/sub/uevent (3 bytes) +-rw------- devices/uevent (3 bytes) +`)}, "substitute": {Mode: fs.ModeDir | 0700}, "work": {Mode: fs.ModeDir | 0700}, diff --git a/pkg/clean_test.go b/pkg/clean_test.go index e31d52aa..3b5d0b3b 100644 --- a/pkg/clean_test.go +++ b/pkg/clean_test.go @@ -52,7 +52,7 @@ func TestClean(t *testing.T) { "lock": {Mode: 0644}, "variant": {Mode: 0400}, - "status": {Mode: fs.ModeDir | 0700}, + "status": {Mode: fs.ModeDir | 0711}, "substitute": {Mode: fs.ModeDir | 0700}, "fault": {Mode: fs.ModeDir | 0700}, "work": {Mode: fs.ModeDir | 0700}, @@ -73,7 +73,7 @@ func TestClean(t *testing.T) { "lock": {Mode: 0644}, "variant": {Mode: 0400}, - "status": {Mode: fs.ModeDir | 0700}, + "status": {Mode: fs.ModeDir | 0711}, "substitute": {Mode: fs.ModeDir | 0700}, "fault": {Mode: fs.ModeDir | 0700}, "work": {Mode: fs.ModeDir | 0700}, @@ -127,7 +127,7 @@ func TestClean(t *testing.T) { "lock": {Mode: 0644}, "variant": {Mode: 0400}, - "status": {Mode: fs.ModeDir | 0700}, + "status": {Mode: fs.ModeDir | 0711}, "substitute": {Mode: fs.ModeDir | 0700}, "substitute/dP-6_wIDRRouOaOF-nkBy-IaUbLdYHbOUTrptu7j_qfW01mnHSjYJ0oykUmvUd2x": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")}, @@ -205,9 +205,9 @@ func TestClean(t *testing.T) { "lock": {Mode: 0644}, "variant": {Mode: 0400}, - "status": {Mode: fs.ModeDir | 0700}, - "status/xxQ43VMIS1WHMSEFnUdBNd6pulbtgiTorP_mtJpEsZENYhGBiDEL6Y2D8yqn0Sra": {Mode: 0400, Data: []byte(statusHeader + "kept")}, - "status/td1tf1kb3z8iUFg2k1xlHeTTEz3xR4e77WSWdv2JhjmZyY6iuvodhdT8BblDOi0E": {Mode: 0400, Data: []byte(statusHeader + "kept")}, + "status": {Mode: fs.ModeDir | 0711}, + "status/xxQ43VMIS1WHMSEFnUdBNd6pulbtgiTorP_mtJpEsZENYhGBiDEL6Y2D8yqn0Sra": {Mode: 0444, Data: []byte(statusHeader + "kept")}, + "status/td1tf1kb3z8iUFg2k1xlHeTTEz3xR4e77WSWdv2JhjmZyY6iuvodhdT8BblDOi0E": {Mode: 0444, Data: []byte(statusHeader + "kept")}, "substitute": {Mode: fs.ModeDir | 0700}, "substitute/td1tf1kb3z8iUFg2k1xlHeTTEz3xR4e77WSWdv2JhjmZyY6iuvodhdT8BblDOi0E": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/CyDnDvF-LaeGPcSW70tPosNCoclByWkTjznUUF1DcgzlIwkN9yzz1ZFME1TlPj6W")}, diff --git a/pkg/exec.go b/pkg/exec.go index b40ae84b..fe099ac9 100644 --- a/pkg/exec.go +++ b/pkg/exec.go @@ -601,7 +601,12 @@ func (a *execArtifact) makeContainer( fhs.AbsTmp, std.BindWritable|std.BindEnsure, ) - z.Proc(fhs.AbsProc).Dev(fhs.AbsDev, true) + z. + Proc(fhs.AbsProc). + DevWritable(fhs.AbsDev, true). + Tmpfs(fhs.AbsDevShm, 0, 01777). + Bind(fhs.AbsDevKvm, fhs.AbsDevKvm, std.BindOptional|std.BindDevice). + Remount(fhs.AbsDev, syscall.MS_RDONLY) return } diff --git a/pkg/exec_test.go b/pkg/exec_test.go index 051af551..a24c1e5e 100644 --- a/pkg/exec_test.go +++ b/pkg/exec_test.go @@ -23,18 +23,18 @@ import ( "hakurei.app/internal/stub" "hakurei.app/pkg" - "hakurei.app/pkg/internal/testtool/expected" + "hakurei.app/pkg/internal/tester/expected" ) -// testtoolBin is the container test tool binary made available to the +// testHelperBin is the container test helper binary made available to the // execArtifact for testing its curing environment. // -//go:generate env CGO_ENABLED=0 go build -tags testtool -o internal/testtool ./internal/testtool -//go:embed internal/testtool/testtool -var testtoolBin []byte +//go:generate env CGO_ENABLED=0 go build -tags tester -o internal/tester ./internal/tester +//go:embed internal/tester/tester +var testHelperBin []byte func init() { - pathname, err := filepath.Abs("internal/testtool/testtool") + pathname, err := filepath.Abs("internal/tester/tester") if err != nil { panic(err) } @@ -153,15 +153,15 @@ func TestExec(t *testing.T) { checkWithCache(t, []cacheTestCase{ {"offline", pkg.CValidateKnown | checkDestroySubstitutes, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { - testtool, testtoolDestroy := newTesttool() + testHelper, destroyTestHelper := newTestHelper() cureMany(t, c, []cureStep{ {"container", pkg.NewExec( "exec-offline", "", new(wantOffline.hash()), 0, false, false, pkg.AbsWork, []string{"HAKUREI_TEST=1"}, - check.MustAbs("/opt/bin/testtool"), - []string{"testtool"}, + check.MustAbs("/opt/bin/tester"), + []string{"tester"}, pkg.MustPath("/file", false, newStubFile( pkg.KindHTTPGet, @@ -176,15 +176,15 @@ func TestExec(t *testing.T) { return os.MkdirAll(t.GetWorkDir().String(), 0700) }, }), - pkg.MustPath("/opt", false, testtool), + pkg.MustPath("/opt", false, testHelper), ), ignorePathname, wantOffline, pkg.WNew, nil}, {"substitution", pkg.NewExec( "exec-offline", "", new(wantOffline.hash()), 0, false, false, pkg.AbsWork, []string{"HAKUREI_TEST=1"}, - check.MustAbs("/opt/bin/testtool"), - []string{"testtool"}, + check.MustAbs("/opt/bin/tester"), + []string{"tester"}, pkg.MustPath("/file", false, newStubFile( pkg.KindHTTPGet, @@ -192,7 +192,7 @@ func TestExec(t *testing.T) { nil, nil, nil, )), - // substitution miss fails in testtool due to differing idents + // substitution miss fails in tester due to differing idents pkg.MustPath("/.hakurei", false, &stubArtifact{ kind: pkg.KindTar, params: []byte("empty directory (substituted)"), @@ -200,15 +200,15 @@ func TestExec(t *testing.T) { return os.MkdirAll(t.GetWorkDir().String(), 0700) }, }), - pkg.MustPath("/opt", false, testtool), + pkg.MustPath("/opt", false, testHelper), ), ignorePathname, wantOffline, pkg.WSubstitute, nil}, {"error passthrough", pkg.NewExec( "", "", nil, 0, false, true, pkg.AbsWork, []string{"HAKUREI_TEST=1"}, - check.MustAbs("/opt/bin/testtool"), - []string{"testtool"}, + check.MustAbs("/opt/bin/tester"), + []string{"tester"}, pkg.MustPath("/proc/nonexistent", false, failingArtifact), ), nil, nil, pkg.WNew, pkg.InputError{ @@ -219,8 +219,8 @@ func TestExec(t *testing.T) { "", "", nil, 0, false, false, pkg.AbsWork, []string{"HAKUREI_TEST=1"}, - check.MustAbs("/opt/bin/testtool"), - []string{"testtool"}, + check.MustAbs("/opt/bin/tester"), + []string{"tester"}, pkg.ExecPath{}, ), nil, nil, pkg.WNew, pkg.ErrInvalidPaths}, @@ -231,8 +231,8 @@ func TestExec(t *testing.T) { "", "", nil, 0, false, false, pkg.AbsWork, nil, - check.MustAbs("/opt/bin/testtool"), - []string{"testtool"}, + check.MustAbs("/opt/bin/tester"), + []string{"tester"}, ) var exitError *exec.ExitError if _, _, err := c.Cure(initFailureArtifact); !errors.As(err, &exitError) || @@ -256,13 +256,13 @@ func TestExec(t *testing.T) { if !bytes.HasPrefix(faultStatus, []byte( "hakurei.app/pkg ", )) || !bytes.Contains(faultStatus, []byte( - "\ninit: fork/exec /opt/bin/testtool: no such file or directory\n", + "\ninit: fork/exec /opt/bin/tester: no such file or directory\n", )) { t.Errorf("unexpected status:\n%s", string(faultStatus)) } destroyStatus(t, base, 2, 1) - testtoolDestroy(t, base, c) + destroyTestHelper(t, base, c) }, expectsFS{ ".": {Mode: fs.ModeDir | 0700}, @@ -286,7 +286,7 @@ func TestExec(t *testing.T) { }}, {"net", pkg.CValidateKnown | checkDestroySubstitutes, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { - testtool, testtoolDestroy := newTesttool() + testHelper, destroyTestHelper := newTestHelper() wantNet := expectsFS{ ".": {Mode: fs.ModeDir | 0500}, @@ -298,8 +298,8 @@ func TestExec(t *testing.T) { "exec-net", "", new(wantNet.hash()), 0, true, false, pkg.AbsWork, []string{"HAKUREI_TEST=1"}, - check.MustAbs("/opt/bin/testtool"), - []string{"testtool", "net"}, + check.MustAbs("/opt/bin/tester"), + []string{"tester", "net"}, pkg.MustPath("/file", false, newStubFile( pkg.KindHTTPGet, @@ -314,12 +314,12 @@ func TestExec(t *testing.T) { return os.MkdirAll(t.GetWorkDir().String(), 0700) }, }), - pkg.MustPath("/opt", false, testtool), + pkg.MustPath("/opt", false, testHelper), ), ignorePathname, wantNet, pkg.WNew, nil}, }) destroyStatus(t, base, 2, 0) - testtoolDestroy(t, base, c) + destroyTestHelper(t, base, c) }, expectsFS{ ".": {Mode: fs.ModeDir | 0700}, @@ -341,15 +341,15 @@ func TestExec(t *testing.T) { }}, {"overlay root", pkg.CValidateKnown | checkDestroySubstitutes, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { - testtool, testtoolDestroy := newTesttool() + testHelper, destroyTestHelper := newTestHelper() cureMany(t, c, []cureStep{ {"container", pkg.NewExec( "exec-overlay-root", "", nil, 0, false, false, pkg.AbsWork, []string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"}, - check.MustAbs("/opt/bin/testtool"), - []string{"testtool"}, + check.MustAbs("/opt/bin/tester"), + []string{"tester"}, pkg.MustPath("/", true, &stubArtifact{ kind: pkg.KindTar, @@ -358,12 +358,12 @@ func TestExec(t *testing.T) { return os.MkdirAll(t.GetWorkDir().String(), 0700) }, }), - pkg.MustPath("/opt", false, testtool), + pkg.MustPath("/opt", false, testHelper), ), ignorePathname, wantOffline, pkg.WNew, nil}, }) destroyStatus(t, base, 2, 0) - testtoolDestroy(t, base, c) + destroyTestHelper(t, base, c) }, expectsFS{ ".": {Mode: fs.ModeDir | 0700}, @@ -383,15 +383,15 @@ func TestExec(t *testing.T) { }}, {"overlay work", pkg.CValidateKnown | checkDestroySubstitutes, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { - testtool, testtoolDestroy := newTesttool() + testHelper, destroyTestHelper := newTestHelper() cureMany(t, c, []cureStep{ {"container", pkg.NewExec( "exec-overlay-work", "", nil, 0, false, false, pkg.AbsWork, []string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"}, - check.MustAbs("/work/bin/testtool"), - []string{"testtool"}, + check.MustAbs("/work/bin/tester"), + []string{"tester"}, pkg.MustPath("/", true, &stubArtifact{ kind: pkg.KindTar, @@ -405,12 +405,12 @@ func TestExec(t *testing.T) { cure: func(t *pkg.TContext) error { return os.MkdirAll(t.GetWorkDir().String(), 0700) }, - }), pkg.Path(pkg.AbsWork, false /* ignored */, testtool), + }), pkg.Path(pkg.AbsWork, false /* ignored */, testHelper), ), ignorePathname, wantOffline, pkg.WNew, nil}, }) destroyStatus(t, base, 2, 0) - testtoolDestroy(t, base, c) + destroyTestHelper(t, base, c) }, expectsFS{ ".": {Mode: fs.ModeDir | 0700}, @@ -430,15 +430,15 @@ func TestExec(t *testing.T) { }}, {"multiple layers", pkg.CValidateKnown | checkDestroySubstitutes, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { - testtool, testtoolDestroy := newTesttool() + testHelper, destroyTestHelper := newTestHelper() cureMany(t, c, []cureStep{ {"container", pkg.NewExec( "exec-multiple-layers", "", nil, 0, false, false, pkg.AbsWork, []string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"}, - check.MustAbs("/opt/bin/testtool"), - []string{"testtool", "layers"}, + check.MustAbs("/opt/bin/tester"), + []string{"tester", "layers"}, pkg.MustPath("/", true, &stubArtifact{ kind: pkg.KindTar, @@ -474,12 +474,12 @@ func TestExec(t *testing.T) { return os.WriteFile(work.Append("check").String(), []byte("layers"), 0400) }, }), - pkg.MustPath("/opt", false, testtool), + pkg.MustPath("/opt", false, testHelper), ), ignorePathname, wantOffline, pkg.WNew, nil}, }) destroyStatus(t, base, 2, 0) - testtoolDestroy(t, base, c) + destroyTestHelper(t, base, c) }, expectsFS{ ".": {Mode: fs.ModeDir | 0700}, @@ -504,15 +504,15 @@ func TestExec(t *testing.T) { }}, {"overlay layer promotion", pkg.CValidateKnown | checkDestroySubstitutes, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { - testtool, testtoolDestroy := newTesttool() + testHelper, destroyTestHelper := newTestHelper() cureMany(t, c, []cureStep{ {"container", pkg.NewExec( "exec-layer-promotion", "", nil, 0, false, true, pkg.AbsWork, []string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"}, - check.MustAbs("/opt/bin/testtool"), - []string{"testtool", "promote"}, + check.MustAbs("/opt/bin/tester"), + []string{"tester", "promote"}, pkg.MustPath("/", true, &stubArtifact{ kind: pkg.KindTar, @@ -527,12 +527,12 @@ func TestExec(t *testing.T) { return os.MkdirAll(t.GetWorkDir().String(), 0700) }, }), - pkg.MustPath("/opt", false, testtool), + pkg.MustPath("/opt", false, testHelper), ), ignorePathname, wantOffline, pkg.WNew, nil}, }) destroyStatus(t, base, 2, 0) - testtoolDestroy(t, base, c) + destroyTestHelper(t, base, c) }, expectsFS{ ".": {Mode: fs.ModeDir | 0700}, @@ -621,14 +621,14 @@ func TestExec(t *testing.T) { }) } -// newTesttool returns an [Artifact] that cures into testtoolBin. The returned +// newTestHelper returns an [Artifact] that cures into testHelperBin. The returned // function must be called at the end of the test but not deferred. -func newTesttool() ( - testtool pkg.Artifact, - testtoolDestroy func(t *testing.T, base *check.Absolute, c *pkg.Cache), +func newTestHelper() ( + testHelper pkg.Artifact, + destroyTestHelper func(t *testing.T, base *check.Absolute, c *pkg.Cache), ) { - // testtoolBin is built during go:generate and is not deterministic - testtool = overrideIdent{pkg.ID{0xfe, 0xff}, &stubArtifact{ + // testHelperBin is built during go:generate and is not deterministic + testHelper = overrideIdent{pkg.ID{0xfe, 0xff}, &stubArtifact{ kind: pkg.KindTar, cure: func(t *pkg.TContext) error { work := t.GetWorkDir() @@ -661,10 +661,10 @@ func newTesttool() ( return os.WriteFile(t.GetWorkDir().Append( "bin", - "testtool", - ).String(), testtoolBin, 0500) + "tester", + ).String(), testHelperBin, 0500) }, }} - testtoolDestroy = newDestroyArtifactFunc(testtool) + destroyTestHelper = newDestroyArtifactFunc(testHelper) return } diff --git a/pkg/internal/testtool/expected/expected.go b/pkg/internal/tester/expected/expected.go index c0e8e4a2..c0e8e4a2 100644 --- a/pkg/internal/testtool/expected/expected.go +++ b/pkg/internal/tester/expected/expected.go diff --git a/pkg/internal/tester/expected/sum_amd64.go b/pkg/internal/tester/expected/sum_amd64.go new file mode 100644 index 00000000..c3ef87b6 --- /dev/null +++ b/pkg/internal/tester/expected/sum_amd64.go @@ -0,0 +1,11 @@ +package expected + +const ( + Offline = "8cvRbO_908FqbZvom0pp2MdLHiZMlllsBBHtwJMDOPeIm_Y55AvL8nkB5-ZvKQP7" + OfflineS = "R4nw81-Egce4yJgi4eoK0nDzPNkvCffaPUn8KPP1q1sdQ-dxjGc6YaYtBViUZTz4" + OvlRoot = "0W_DBfLbYbYiXkCL9BIwq1BBL_CAOQw1Al75FmU-w1pYWxc-s1CpaUl2VGiK1wU6" + Layers = "X0e6BdvAC_-MOguDI8f_9OrLyv9r32nV4zKRkLMr2Xye0-Nlc3Rew9Cycm0FlNfb" + Net = "B4VXPvG7mhh96PWmjwwma8Fy4HliP9WFEEW5vtq2lQrPEulZT_lSixX7Iowqpe_J" + Promote = "T_WzFNqwS4hZvNXsnDKOQkc7iwl38CY0GUsCGVnAhiJkU7iLARbeNDtAcA7C-FX1" + Work = "HknOKpTaBKOA5hwAP4eiqwzOQrw9JPQpJVhf-mpd2mIpQc6dfP7xqDALV2h4z0Ce" +) diff --git a/pkg/internal/tester/expected/sum_arm64.go b/pkg/internal/tester/expected/sum_arm64.go new file mode 100644 index 00000000..3b0d3a5b --- /dev/null +++ b/pkg/internal/tester/expected/sum_arm64.go @@ -0,0 +1,11 @@ +package expected + +const ( + Offline = "f2G7Tr0wDhFL8LuJcgggob3C6jjVUvR1Vrs_xIQVaFt7qaoZJwtWIZ3-KgI0BjAE" + OfflineS = "6ShIakYroFkApzv_spmRfdhndwnnUVZFD2fgBRVA8n6GiSuYoeqF_37Loun6DneC" + OvlRoot = "RDokIPUWpf6kGWaaOtowwNGkeBFOIyQBjUY3sQbJ_KDnioKTQTKn2kijDdQ9L6Fp" + Layers = "dKXuQIWrySwPm5CNlR86XCe6FLrppcAJ6UxQKP75RCIjuK0nXA8ee6FMh4VNmKQS" + Net = "vxarc7CMN1BFQe7jOf4iDKNpz2DVT1Om1KdrVCAZyQd_7mr3loB0GjSnbE-mpjyC" + Promote = "oypLdVJZmZ1c9Tmfk-8N-3EBzgO99MymPS9H5nsCBPtm9CV9woEj8CBtlKk3b7gH" + Work = "JlGKRqXYGu6cREq_D4FIAHRT4zqu7uGIUtppWXDdEzRpQKflh7Y4sNHda1ecxRzj" +) diff --git a/pkg/internal/tester/expected/sum_riscv64.go b/pkg/internal/tester/expected/sum_riscv64.go new file mode 100644 index 00000000..77c58120 --- /dev/null +++ b/pkg/internal/tester/expected/sum_riscv64.go @@ -0,0 +1,11 @@ +package expected + +const ( + Offline = "Xa9l5ZZx7lt0B0YZeHihzWa6P6bGQT3rYn2kaftMDqZ7L6bpE9eCG9_e1dWlz-pn" + OfflineS = "6_GnlF6L7phCGuzeG91SAXCgxYPMJqv3UsZVvvoTdCvTtl8nCKnYxTlxAjdTNYY_" + OvlRoot = "2VZwLevuQ_ZmwFbAS-eZ4jGjcc71VqQYMkbZgzsV175oL3hGMto6jK-kFp6tcOF9" + Layers = "QukD3gaFOC-1wSm4XvdLtkZllw5APdTSX4mcWT88XEDNGMhvcblgaOr6Q1a20D7W" + Net = "XJME3Hjy2iZ2kGepJXtUOhETEMD6Li9ccLwi7y_4vQ7Kur0KGpXrfM8tyFnfLkFg" + Promote = "l71HASLptywY1pYL_pJBD8qoBkuUJCUobA6mSLUIsajew1IYkscZckBsqPu_czYn" + Work = "ZTJD0WoYxSB15h_VPjXHMuLhlL2bMgyGrJgltHRd4XBZhNr-WYNGVodt53GCBRXC" +) diff --git a/pkg/internal/testtool/main.go b/pkg/internal/tester/main.go index edffab7e..72e20426 100644 --- a/pkg/internal/testtool/main.go +++ b/pkg/internal/tester/main.go @@ -1,5 +1,6 @@ -//go:build testtool +//go:build tester +// The tester program checks containers started by package pkg. package main import ( @@ -17,12 +18,12 @@ import ( "hakurei.app/fhs" "hakurei.app/vfs" - "hakurei.app/pkg/internal/testtool/expected" + "hakurei.app/pkg/internal/tester/expected" ) func main() { log.SetFlags(0) - log.SetPrefix("testtool: ") + log.SetPrefix("tester: ") if os.Getenv("HAKUREI_BINFMT") == "1" { wantArgs := []string{"/interpreter", "/opt/bin/sample"} @@ -50,27 +51,27 @@ func main() { }) var hostNet, layers, promote bool - if len(os.Args) == 2 && os.Args[0] == "testtool" { + if len(os.Args) == 2 && os.Args[0] == "tester" { switch os.Args[1] { case "net": hostNet = true - log.SetPrefix("testtool(net): ") + log.SetPrefix("tester(net): ") break case "layers": layers = true - log.SetPrefix("testtool(layers): ") + log.SetPrefix("tester(layers): ") break case "promote": promote = true - log.SetPrefix("testtool(promote): ") + log.SetPrefix("tester(promote): ") default: log.Fatalf("Args: %q", os.Args) return } - } else if wantArgs := []string{"testtool"}; !slices.Equal(os.Args, wantArgs) { + } else if wantArgs := []string{"tester"}; !slices.Equal(os.Args, wantArgs) { log.Fatalf("Args: %q, want %q", os.Args, wantArgs) } @@ -79,7 +80,7 @@ func main() { if len(environ) == 2 { overlayRoot = true if !layers && !promote { - log.SetPrefix("testtool(overlay root): ") + log.SetPrefix("tester(overlay root): ") } wantEnv = []string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"} } @@ -89,8 +90,8 @@ func main() { var overlayWork bool const ( - wantExec = "/opt/bin/testtool" - wantExecWork = "/work/bin/testtool" + wantExec = "/opt/bin/tester" + wantExecWork = "/work/bin/tester" ) var iftPath string if got, err := os.Executable(); err != nil { @@ -102,7 +103,7 @@ func main() { switch got { case wantExecWork: overlayWork = true - log.SetPrefix("testtool(overlay work): ") + log.SetPrefix("tester(overlay work): ") default: log.Fatalf("Executable: %q, want %q", got, wantExec) @@ -225,7 +226,7 @@ func main() { ident = expected.Promote } - next() // testtool artifact + next() // test helper artifact next() if overlayWork { @@ -259,6 +260,10 @@ func main() { next() } + if m.Next != nil && m.Next.Root == "/kvm" { + next() + } + if m.Next != nil { log.Println("unexpected extra mount entries") for m.Next != nil { diff --git a/pkg/internal/testtool/expected/sum_amd64.go b/pkg/internal/testtool/expected/sum_amd64.go deleted file mode 100644 index 354931be..00000000 --- a/pkg/internal/testtool/expected/sum_amd64.go +++ /dev/null @@ -1,11 +0,0 @@ -package expected - -const ( - Offline = "IKmVhALejtYA3JhN1PCfjKF-Dio1XVcOeMWDHhfSHCbb4iBNhGjFy2UspZveZvb8" - OfflineS = "6ZubjSyCnz4-TKgQZi_BbY-JUwsStYCtD_f-BTDOOotfEbqkm47W20AazHRaLviz" - OvlRoot = "Yox7WpG8B_OqIXWF4HWFZkUfsEl7DGzpFyZM0qtd9T8nQ-7SAkEUurlAe6_DJ6Iu" - Layers = "aAwfxDB-efVo379FNl178jK7HEKU3LEjHbZdaVVjk7GnXGZGEANNsXnB3SCUxM9A" - Net = "IU3GwU2jTBd7HjotvFo2wsDWBQGVGpamwiSKdc2G1EnhM-ZaacxAuK6q0wbTVxra" - Promote = "rOjetGHzT_jPErrBR24IAZKDj8Q9dLLqxFQqloMgjsULss0VXDI--n6LxuMBHvmZ" - Work = "7f9XDeREfvJIjCe0qesND0s0tasExAIcc-OH33-eq9tsrQPUXhJp4KEkpsYH4hCD" -) diff --git a/pkg/internal/testtool/expected/sum_arm64.go b/pkg/internal/testtool/expected/sum_arm64.go deleted file mode 100644 index 2e6f7cbf..00000000 --- a/pkg/internal/testtool/expected/sum_arm64.go +++ /dev/null @@ -1,11 +0,0 @@ -package expected - -const ( - Offline = "MTgCSzwPrzGigp9phvDPRwUTNGLm1KM2EsEtbz-l2cNieqjaGtnzm65CDXlSxXrl" - OfflineS = "p89bQg7_jR4nwaSbpEd08ne1jHOEw7Wlnlyv5FHq7ba1Kml3HSBZwEKRzu0gCqEb" - OvlRoot = "9ifwO6MAaV9kTqRR2O5PJzm8XhfXbr50mFLWyEHQm8OHHcJtAEUgXZzJYoZt9YVV" - Layers = "INuGActFPwJtS7mAR9Bkt7FD4yoHlXHSJ9CaSAQJrpkUBL_k815Cs_jvRfxy5-a7" - Net = "kJvy24uk4ePvHoxSKf4vMOhR4NxLbmyWlA9bTS7d3z3fE9WF1Tvpao9g-QuYU4vm" - Promote = "lYqyYrfjPawIbld5Qz0NbKvd_bmI3nJC1eEnlIQWzsjWE9uA1HuKPAaClnDeyG6L" - Work = "V6k4FqOBMRCmHiqchs5-joVGZ_MhNWZyXm3gdbU0S7S5eMze7G4SwYMTf66ojKQL" -) diff --git a/pkg/internal/testtool/expected/sum_riscv64.go b/pkg/internal/testtool/expected/sum_riscv64.go deleted file mode 100644 index 53872c08..00000000 --- a/pkg/internal/testtool/expected/sum_riscv64.go +++ /dev/null @@ -1,11 +0,0 @@ -package expected - -const ( - Offline = "LlieSHVDe1lnKvfI8t4oGRg_2hPxwyy1drmaqc-EKCOENuS9C2YHrp2VY5pSyhbg" - OfflineS = "qccjM8oYjUuEYPKFFzAxDFVzMerSUKUZc18NdVQoiPWf-70XOsNK8NaUQqjTgTAD" - OvlRoot = "lpV2bb_KGYFHPiOn-jtGop8K_ew0y1p3BezRp0ebM93_a-T9J-vkqGD5uokda1aN" - Layers = "sM3DJm210CHPGHkAquIbrXhspB81FpVC60ApQrvJGD5El2i7cSONj7LWf7EWqKpT" - Net = "JihQUwsVETRYjb2-yIXLYZ6WVsaFebwl8btSYAsNQ2QapS5MZVaI-dYwrVoySycR" - Promote = "lGqwR4W2OjylSVitMQP1lc6wApOBkbMjsmIqkxQbfEZ3DHEQBbJr84ErW6Xb54zo" - Work = "aZgtyv6aMtoHFkHAaJr6nLaA7N_SHjShgWPyQ5UdzaIqYSiVqvCrZqSokGTCcTii" -) diff --git a/pkg/ir_test.go b/pkg/ir_test.go index 9cc024ae..7ad5a8f5 100644 --- a/pkg/ir_test.go +++ b/pkg/ir_test.go @@ -40,8 +40,8 @@ func TestIRRoundtrip(t *testing.T) { "exec-offline", "", nil, 0, false, false, pkg.AbsWork, []string{"HAKUREI_TEST=1"}, - check.MustAbs("/opt/bin/testtool"), - []string{"testtool"}, + check.MustAbs("/opt/bin/tester"), + []string{"tester"}, pkg.MustPath("/file", false, pkg.NewFile("file", []byte( "stub file", @@ -49,7 +49,7 @@ func TestIRRoundtrip(t *testing.T) { nil, "file:///hakurei.tar", pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))), ))), pkg.MustPath("/opt", false, pkg.NewTar(pkg.NewDecompress(pkg.NewHTTPGet( - nil, "file:///testtool.tar.gz", + nil, "file:///test-helper.tar.gz", pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))), ), pkg.Gzip))), )}, @@ -60,8 +60,8 @@ func TestIRRoundtrip(t *testing.T) { 0, false, false, pkg.AbsWork, []string{"HAKUREI_TEST=1"}, - check.MustAbs("/opt/bin/testtool"), - []string{"testtool", "net"}, + check.MustAbs("/opt/bin/tester"), + []string{"tester", "net"}, pkg.MustPath("/file", false, pkg.NewFile("file", []byte( "stub file", @@ -69,7 +69,7 @@ func TestIRRoundtrip(t *testing.T) { nil, "file:///hakurei.tar", pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))), ))), pkg.MustPath("/opt", false, pkg.NewTar(pkg.NewDecompress(pkg.NewHTTPGet( - nil, "file:///testtool.tar.gz", + nil, "file:///test-helper.tar.gz", pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))), ), pkg.Gzip))), )}, @@ -80,8 +80,8 @@ func TestIRRoundtrip(t *testing.T) { 0, false, false, pkg.AbsWork, []string{"HAKUREI_TEST=1"}, - check.MustAbs("/opt/bin/testtool"), - []string{"testtool", "measured"}, + check.MustAbs("/opt/bin/tester"), + []string{"tester", "measured"}, pkg.MustPath("/file", false, pkg.NewFile("file", []byte( "stub file", @@ -89,7 +89,7 @@ func TestIRRoundtrip(t *testing.T) { nil, "file:///hakurei.tar", pkg.Checksum(bytes.Repeat([]byte{0xfd}, len(pkg.Checksum{}))), ))), pkg.MustPath("/opt", false, pkg.NewTar(pkg.NewDecompress(pkg.NewHTTPGet( - nil, "file:///testtool.tar.gz", + nil, "file:///test-helper.tar.gz", pkg.Checksum(bytes.Repeat([]byte{0xfd}, len(pkg.Checksum{}))), ), pkg.Gzip))), )}, @@ -206,7 +206,7 @@ func (t *TContext) prepareStatus(writeHeader bool) error { if t.status, t.statusErr = os.OpenFile( t.statusPath.String(), syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, - 0400, + 0444, ); t.statusErr != nil { return t.statusErr } @@ -364,6 +364,9 @@ func (f *FContext) linkSubstitute(ids, substitutes string) (err error) { if err == nil { f.statusSPath = statusS } + if errors.Is(err, os.ErrExist) { + err = nil + } return } @@ -2798,7 +2801,6 @@ func Open( dirSubstitute, dirIdentifier, dirChecksum, - dirStatus, dirFault, dirWork, } { @@ -2810,6 +2812,17 @@ func Open( } } + for _, name := range []string{ + dirStatus, + } { + if err := os.MkdirAll( + base.Append(name).String(), + 0711, + ); err != nil && !errors.Is(err, os.ErrExist) { + return nil, err + } + } + c := Cache{ parent: ctx, @@ -2923,6 +2936,9 @@ func Open( return &c, nil } +// Path returns the pathname of the directory holding [Cache] state. +func (c *Cache) Path() *check.Absolute { return c.base } + // Collected is returned by [Collect.Cure] to indicate a successful collection. type Collected struct{} diff --git a/pkg/pkg_test.go b/pkg/pkg_test.go index aa5016c1..66639b5b 100644 --- a/pkg/pkg_test.go +++ b/pkg/pkg_test.go @@ -587,7 +587,9 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) { // destroy empty fault directory if err := os.Remove(base.Append("fault").String()); err != nil { - t.Fatal(err) + if !errors.Is(err, syscall.ENOTEMPTY) { + t.Fatal(err) + } } want := tc.want.hash() @@ -738,6 +740,16 @@ func newWantScrubError(base *check.Absolute) *pkg.ScrubError { } } +// dropFaults removes all fault entries from a cache under testing. +func dropFaults(t *testing.T, base *check.Absolute) { + fault := base.Append("fault") + if err := os.RemoveAll(fault.String()); err != nil { + t.Fatal(err) + } else if err = os.Mkdir(fault.String(), 0); err != nil { + t.Fatal(err) + } +} + func TestCache(t *testing.T) { t.Parallel() @@ -1512,10 +1524,10 @@ func TestCache(t *testing.T) { "identifier/heRgWhNJmhm898V68kw0ta76t4rP1o29tYW_TrdyBWXGfo37K3QWA7Z-JkLXAwcn": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")}, "identifier/q-VoVBI3IjcWTqwIokpi3y05CP113CHgHslc-XWnhOvDULwzVc3q9Y5xOCm_YyLZ": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")}, - "status": {Mode: fs.ModeDir | 0700}, + "status": {Mode: fs.ModeDir | 0711}, "status/q-VoVBI3IjcWTqwIokpi3y05CP113CHgHslc-XWnhOvDULwzVc3q9Y5xOCm_YyLZ": {Mode: fs.ModeSymlink | 0777, Data: []byte("dP-6_wIDRRouOaOF-nkBy-IaUbLdYHbOUTrptu7j_qfW01mnHSjYJ0oykUmvUd2x")}, - "status/heRgWhNJmhm898V68kw0ta76t4rP1o29tYW_TrdyBWXGfo37K3QWA7Z-JkLXAwcn": {Mode: 0400, Data: []byte(statusHeader + "destroyed")}, - "status/dP-6_wIDRRouOaOF-nkBy-IaUbLdYHbOUTrptu7j_qfW01mnHSjYJ0oykUmvUd2x": {Mode: 0400, Data: []byte(statusHeader + "destroyed")}, + "status/heRgWhNJmhm898V68kw0ta76t4rP1o29tYW_TrdyBWXGfo37K3QWA7Z-JkLXAwcn": {Mode: 0444, Data: []byte(statusHeader + "destroyed")}, + "status/dP-6_wIDRRouOaOF-nkBy-IaUbLdYHbOUTrptu7j_qfW01mnHSjYJ0oykUmvUd2x": {Mode: 0444, Data: []byte(statusHeader + "destroyed")}, "substitute": {Mode: fs.ModeDir | 0700}, "substitute/dP-6_wIDRRouOaOF-nkBy-IaUbLdYHbOUTrptu7j_qfW01mnHSjYJ0oykUmvUd2x": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")}, @@ -1610,14 +1622,16 @@ func TestCache(t *testing.T) { "checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl/result": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent")}, "identifier": {Mode: fs.ModeDir | 0700}, - "identifier/k8yyhlBV3JpYzzywAnrRWkfp2TmgIDNC6FqMFwrrR2x0C3unBLveDNKgigOjG9MM": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, - "identifier/Xys8hPpJsKirZmTTuT7SwQ6661TCp_ZRu8iUjPPD7pC5E3ll7xw4zhNlZ2PVcz_-": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")}, "identifier/8sInO_dhPW9kweit7jVuH_vtMkRxlZT7XE4_yEKPZmpGkgVxkmxz0OpXgbMsH6ET": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, + "identifier/9_LKKy-vVT9DxQj-SkqSJFZAc0Yehan3LhldTw8fOIP4XX_DrvkpcPkBV5_46e2q": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")}, + "identifier/kHH2amyzTN_EpsOyKEOenCROONllTrWAXAwVXOKzx3TgM95-y9ipiz0vkRE-ujBf": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, "identifier/ofaXbB5gIza_7W4Rzo5XDe_8FwlCPKWn3TBcmGgFAkkTWMl5GXGmOzX89Jd2Mk2A": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")}, "identifier/8VL-egZySw-RbnHZtxGWdf5Z2v9S9CihrdFwa54Bc5EYMQcEgWEem7QrdCTWlCb8": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")}, - "status": {Mode: fs.ModeDir | 0700}, - "status/8VL-egZySw-RbnHZtxGWdf5Z2v9S9CihrdFwa54Bc5EYMQcEgWEem7QrdCTWlCb8": {Mode: 0400, Data: []byte("\x00")}, + "status": {Mode: fs.ModeDir | 0711}, + "status/8VL-egZySw-RbnHZtxGWdf5Z2v9S9CihrdFwa54Bc5EYMQcEgWEem7QrdCTWlCb8": {Mode: 0444, Data: []byte("\x00")}, + "status/9_LKKy-vVT9DxQj-SkqSJFZAc0Yehan3LhldTw8fOIP4XX_DrvkpcPkBV5_46e2q": {Mode: 0444, Data: []byte(statusHeader + "Lrwxrwxrwx result linkname /proc/nonexistent\n")}, + "status/kHH2amyzTN_EpsOyKEOenCROONllTrWAXAwVXOKzx3TgM95-y9ipiz0vkRE-ujBf": {Mode: 0444, Data: []byte(statusHeader)}, "substitute": {Mode: fs.ModeDir | 0700}, "substitute/socGRnhdsJlJ4Z01ZwHFZdLzFH0rWvoaJrylcUKtvhBdGsE7VoYl-vdUuvfK8B4I": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, @@ -1756,17 +1770,19 @@ func TestCache(t *testing.T) { "identifier/3fYJjSiWhklt7TK9MPVLGk5_8AIU8ostTcFPXULJf_qeAVG6pmC-Cqqu4ZhEXRxn": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")}, "identifier/8VL-egZySw-RbnHZtxGWdf5Z2v9S9CihrdFwa54Bc5EYMQcEgWEem7QrdCTWlCb8": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")}, "identifier/8sInO_dhPW9kweit7jVuH_vtMkRxlZT7XE4_yEKPZmpGkgVxkmxz0OpXgbMsH6ET": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, - "identifier/Xys8hPpJsKirZmTTuT7SwQ6661TCp_ZRu8iUjPPD7pC5E3ll7xw4zhNlZ2PVcz_-": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")}, - "identifier/k8yyhlBV3JpYzzywAnrRWkfp2TmgIDNC6FqMFwrrR2x0C3unBLveDNKgigOjG9MM": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, + "identifier/9_LKKy-vVT9DxQj-SkqSJFZAc0Yehan3LhldTw8fOIP4XX_DrvkpcPkBV5_46e2q": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")}, + "identifier/kHH2amyzTN_EpsOyKEOenCROONllTrWAXAwVXOKzx3TgM95-y9ipiz0vkRE-ujBf": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, "identifier/mXlNz_v77nW7jstn_rJn4RL7dCQlkakM0y-HjO6tkDubtzaj-wCmW1hIr2Gk64_O": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")}, - "status": {Mode: fs.ModeDir | 0700}, - "status/8VL-egZySw-RbnHZtxGWdf5Z2v9S9CihrdFwa54Bc5EYMQcEgWEem7QrdCTWlCb8": {Mode: 0400, Data: []byte("\x00")}, + "status": {Mode: fs.ModeDir | 0711}, + "status/8VL-egZySw-RbnHZtxGWdf5Z2v9S9CihrdFwa54Bc5EYMQcEgWEem7QrdCTWlCb8": {Mode: 0444, Data: []byte("\x00")}, + "status/9_LKKy-vVT9DxQj-SkqSJFZAc0Yehan3LhldTw8fOIP4XX_DrvkpcPkBV5_46e2q": {Mode: 0444, Data: []byte(statusHeader + "Lrwxrwxrwx result linkname /proc/nonexistent\n")}, + "status/kHH2amyzTN_EpsOyKEOenCROONllTrWAXAwVXOKzx3TgM95-y9ipiz0vkRE-ujBf": {Mode: 0444, Data: []byte(statusHeader)}, "substitute": {Mode: fs.ModeDir | 0700}, - "substitute/socGRnhdsJlJ4Z01ZwHFZdLzFH0rWvoaJrylcUKtvhBdGsE7VoYl-vdUuvfK8B4I": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, "substitute/MccYezQ4pshkeBV4w_aAEIwvZdKHL3OAgAkR3ouZ43EF94Ur1HBHaN7njPfJAj50": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")}, "substitute/VV5W0_DiA1iylaqbKFThp1ZpV_y1ccVS1XyqDnDaXoHQ-EEyPI4XFrQHcHOF2hWG": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")}, + "substitute/socGRnhdsJlJ4Z01ZwHFZdLzFH0rWvoaJrylcUKtvhBdGsE7VoYl-vdUuvfK8B4I": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, "work": {Mode: fs.ModeDir | 0700}, }}, @@ -66,6 +66,9 @@ func (a *tarArtifact) Inputs() []Artifact { // IsExclusive returns false: decompressor and tar reader are fully sequential. func (a *tarArtifact) IsExclusive() bool { return false } +// Revision satisfies [RevisionArtifact] for status behaviour. +func (*tarArtifact) Revision() uint64 { return 0 } + // A DisallowedTypeflagError describes a disallowed typeflag encountered while // unpacking a tarball. type DisallowedTypeflagError byte @@ -76,6 +79,11 @@ func (e DisallowedTypeflagError) Error() string { // Cure cures the [Artifact], producing a directory located at work. func (a *tarArtifact) Cure(t *TContext) (err error) { + var status io.Writer + if status, err = t.GetStatusWriter(); err != nil { + return + } + var r io.ReadCloser if r, err = t.Open(a.f); err != nil { return @@ -110,6 +118,7 @@ func (a *tarArtifact) Cure(t *TContext) (err error) { var header *tar.Header tr := tar.NewReader(r) + msg := t.GetMessage() for header, err = tr.Next(); err == nil; header, err = tr.Next() { typeflag := header.Typeflag if typeflag == 0 { @@ -126,6 +135,7 @@ func (a *tarArtifact) Cure(t *TContext) (err error) { } } + var m string switch typeflag { case tar.TypeReg: var f *os.File @@ -142,6 +152,13 @@ func (a *tarArtifact) Cure(t *TContext) (err error) { } else if err = f.Close(); err != nil { return } + + m = fmt.Sprintf( + "%s %s (%d bytes)", + header.FileInfo().Mode(), + header.Name, + header.Size, + ) break case tar.TypeLink: @@ -151,6 +168,13 @@ func (a *tarArtifact) Cure(t *TContext) (err error) { ); err != nil { return } + + m = fmt.Sprintf( + "%s %s linkname %s", + header.FileInfo().Mode(), + header.Name, + header.Linkname, + ) break case tar.TypeSymlink: @@ -160,6 +184,13 @@ func (a *tarArtifact) Cure(t *TContext) (err error) { ); err != nil { return } + + m = fmt.Sprintf( + "%s %s linkname %s", + header.FileInfo().Mode(), + header.Name, + header.Linkname, + ) break case tar.TypeDir: @@ -170,21 +201,37 @@ func (a *tarArtifact) Cure(t *TContext) (err error) { if err = root.MkdirAll(header.Name, 0700); err != nil { return } + + m = fmt.Sprintf( + "%s %s", + header.FileInfo().Mode(), + header.Name, + ) break case tar.TypeChar: - t.GetMessage().Verbosef( - "%sskipping character device %d, %d %q%s", - t.cache.sgrWarn, header.Devmajor, header.Devminor, header.Name, t.cache.sgrRes, + m = fmt.Sprintf( + "skipping character device %d, %d %q", + header.Devmajor, header.Devminor, header.Name, ) - continue + break case tar.TypeXGlobalHeader: - continue // ignore + m = "skipping PAX global key-value record" + break default: return DisallowedTypeflagError(typeflag) } + + if m != "" { + msg.Verbose(m) + if _, err = io.WriteString(status, m); err != nil { + return + } else if _, err = status.Write([]byte{'\n'}); err != nil { + return + } + } } if errors.Is(err, io.EOF) { err = nil diff --git a/pkg/tar_test.go b/pkg/tar_test.go index 5462714f..e8a42f73 100644 --- a/pkg/tar_test.go +++ b/pkg/tar_test.go @@ -44,6 +44,24 @@ func TestTar(t *testing.T) { } wantExpandEncode := pkg.Encode(wantExpand.hash()) + const ( + wantStatus = `drwx------ checksum/ +drwx------ checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/ +-r-------- checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/check (2 bytes) +drwx------ checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/ +Lrwxrwxrwx checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/libedac.so linkname /proc/nonexistent/libedac.so +drwx------ checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP/lib/pkgconfig/ +drwx------ identifier/ +Lrwxrwxrwx identifier/HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY linkname ../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP +Lrwxrwxrwx identifier/Zx5ZG9BAwegNT3zQwCySuI2ktCXxNgxirkGLFjW4FW06PtojYVaCdtEw8yuntPLa linkname ../checksum/1TL00Qb8dcqayX7wTO8WNaraHvY6b-KCsctLDTrb64QBCmxj_-byK1HdIUwMaFEP +drwx------ work/ +` + + wantStatusExpand = `drwx------ lib/ +Lrwxrwxrwx lib/libedac.so linkname /proc/nonexistent/libedac.so +` + ) + checkWithCache(t, []cacheTestCase{ {"http", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { checkTarHTTP(t, base, c, fstest.MapFS{ @@ -79,8 +97,12 @@ func TestTar(t *testing.T) { "checksum/" + wantEncode + "/work": {Mode: fs.ModeDir | 0500}, "identifier": {Mode: fs.ModeDir | 0700}, - "identifier/snWp53xxNdx6cV5KSJqzyVWzLlTsFd8udOisIu8Rgxly09mGcxufYCoQ5CQrHDyZ": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantEncode)}, - "identifier/v9DUj2R4YK_3ae0m9VuUYI-HJysDDCENu1k10thxioycRzFH1Qejw5bBGukm4IdL": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantEncode)}, + "identifier/82-Dwiovp_DGXPBmnmrTk4XtsduvTBBXxnJTBgyp6avsUhjcqmL9yKQLxkwJJM0x": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantEncode)}, + "identifier/ld6DxwiP2Gj47Ue2L_pFv6NrOth8nkU5W1voMyivd2aVROrorGqPlpbyHUnU1AvI": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantEncode)}, + + "status": {Mode: fs.ModeDir | 0711}, + "status/82-Dwiovp_DGXPBmnmrTk4XtsduvTBBXxnJTBgyp6avsUhjcqmL9yKQLxkwJJM0x": {Mode: 0444, Data: []byte(statusHeader + wantStatus)}, + "status/ld6DxwiP2Gj47Ue2L_pFv6NrOth8nkU5W1voMyivd2aVROrorGqPlpbyHUnU1AvI": {Mode: 0444, Data: []byte(statusHeader + wantStatus)}, "substitute": {Mode: fs.ModeDir | 0700}, @@ -103,8 +125,12 @@ func TestTar(t *testing.T) { "checksum/" + wantExpandEncode + "/libedac.so": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent/libedac.so")}, "identifier": {Mode: fs.ModeDir | 0700}, - "identifier/1SuvgGltcsgqps9tWqjULG8RCdpx_6KVWXdsMd5xPKvdtysOxqpE-3bLu4j0WpGk": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantExpandEncode)}, - "identifier/snWp53xxNdx6cV5KSJqzyVWzLlTsFd8udOisIu8Rgxly09mGcxufYCoQ5CQrHDyZ": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantExpandEncode)}, + "identifier/82-Dwiovp_DGXPBmnmrTk4XtsduvTBBXxnJTBgyp6avsUhjcqmL9yKQLxkwJJM0x": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantExpandEncode)}, + "identifier/fsMFOV4_LoYWbC8GNqL2zjbpzNzT80vPWbSw4L47caKVIta333WIW2TZXoSW8ntH": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantExpandEncode)}, + + "status": {Mode: fs.ModeDir | 0711}, + "status/82-Dwiovp_DGXPBmnmrTk4XtsduvTBBXxnJTBgyp6avsUhjcqmL9yKQLxkwJJM0x": {Mode: 0444, Data: []byte(statusHeader + wantStatusExpand)}, + "status/fsMFOV4_LoYWbC8GNqL2zjbpzNzT80vPWbSw4L47caKVIta333WIW2TZXoSW8ntH": {Mode: 0444, Data: []byte(statusHeader + wantStatusExpand)}, "substitute": {Mode: fs.ModeDir | 0700}, @@ -206,13 +232,13 @@ func checkTarHTTP( {"directory", pkg.NewTar(&tarDir), ignorePathname, want, pkg.WNew, nil}, - {"multiple entries", pkg.NewTar(&tarDirMulti), nil, nil, pkg.WNew, errors.New( + {"multiple entries", pkg.NewTar(&tarDirMulti), nil, nil, pkg.WNew, errors.Join(errors.New( "input directory does not contain a single regular file", - )}, + ))}, - {"bad type", pkg.NewTar(&tarDirType), nil, nil, pkg.WNew, errors.New( + {"bad type", pkg.NewTar(&tarDirType), nil, nil, pkg.WNew, errors.Join(errors.New( "input directory does not contain a single regular file", - )}, + ))}, {"error passthrough", pkg.NewTar(&stubArtifact{ kind: pkg.KindExec, @@ -220,6 +246,8 @@ func checkTarHTTP( cure: func(t *pkg.TContext) error { return stub.UniqueError(0xcafe) }, - }), nil, nil, pkg.WNew, stub.UniqueError(0xcafe)}, + }), nil, nil, pkg.WNew, errors.Join(stub.UniqueError(0xcafe))}, }) + + dropFaults(t, base) } diff --git a/test/configuration.nix b/test/configuration.nix deleted file mode 100644 index 62d8239d..00000000 --- a/test/configuration.nix +++ /dev/null @@ -1,252 +0,0 @@ -{ - lib, - pkgs, - config, - ... -}: -{ - users.users = { - alice = { - isNormalUser = true; - description = "Alice Foobar"; - password = "foobar"; - uid = 1000; - }; - untrusted = { - isNormalUser = true; - description = "Untrusted user"; - password = "foobar"; - uid = 1001; - - # For deny unmapped uid test: - packages = [ config.environment.hakurei.package ]; - }; - }; - - home-manager.users.alice.home.stateVersion = "24.11"; - - # Automatically login on tty1 as a normal user: - services.getty.autologinUser = "alice"; - - security.pam.loginLimits = [ - { - domain = "@users"; - item = "rtprio"; - type = "-"; - value = 1; - } - ]; - - environment = { - systemPackages = with pkgs; [ - # For D-Bus tests: - mako - libnotify - ]; - - variables = { - SWAYSOCK = "/tmp/sway-ipc.sock"; - WLR_RENDERER = "pixman"; - }; - - # To help with OCR: - etc."xdg/foot/foot.ini".text = lib.generators.toINI { } { - main = { - font = "inconsolata:size=14"; - }; - colors = rec { - foreground = "000000"; - background = "ffffff"; - regular2 = foreground; - }; - }; - }; - - fonts.packages = [ pkgs.inconsolata ]; - - # 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; - - # For PulseAudio tests: - security.rtkit.enable = true; - services.pipewire = { - enable = true; - alsa.enable = true; - alsa.support32Bit = true; - pulse.enable = true; - jack.enable = true; - }; - - virtualisation = { - # Hopefully reduces spurious test failures: - memorySize = if pkgs.stdenv.hostPlatform.is32bit then 2046 else 8192; - - 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 Go test compiler performance: - "-smp 16" - ]; - }; - - # Disk image is too small for some tests: - boot.tmp.useTmpfs = true; - - environment.hakurei = { - enable = true; - stateDir = "/var/lib/hakurei"; - users.alice = 0; - - extraHomeConfig = - { config, ... }: - { - # To test merge deduplication: - options._hakurei.stateVersion = lib.mkOption { type = lib.types.str; }; - - config = { - home = { inherit (config._hakurei) stateVersion; }; - _hakurei.stateVersion = "23.05"; - }; - }; - - commonPaths = [ - { - type = "bind"; - src = "/var/tmp"; - write = true; - } - ]; - - apps = { - "cat.gensokyo.extern.bash.linger-timeout" = { - name = "hakurei-check-linger-timeout"; - identity = 9999; - share = pkgs.bash; - packages = [ pkgs.bash ]; - command = '' - sleep infinity & disown - exit - ''; - wait_delay = 1; - enablements = { - wayland = false; - pipewire = false; - }; - }; - - "cat.gensokyo.extern.foot.noEnablements" = { - name = "ne-foot"; - identity = 1; - shareUid = true; - verbose = true; - share = pkgs.foot; - packages = with pkgs; [ - foot - - # For wayland-info: - wayland-utils - ]; - command = "foot"; - enablements = { - dbus = false; - pipewire = false; - }; - }; - - "cat.gensokyo.extern.foot.noEnablements.immediate" = { - name = "ne-foot-immediate"; - identity = 1; - shareUid = true; - verbose = true; - wait_delay = -1; - share = pkgs.foot; - packages = [ ]; - command = "foot"; - enablements = { - dbus = false; - pipewire = false; - }; - }; - - "cat.gensokyo.extern.foot.pulseaudio" = { - name = "pa-foot"; - identity = 2; - verbose = true; - share = pkgs.foot; - packages = [ pkgs.foot ]; - command = "foot"; - enablements.dbus = false; - }; - - "cat.gensokyo.extern.Alacritty.x11" = { - name = "x11-alacritty"; - identity = 1; - shareUid = true; - verbose = true; - share = pkgs.alacritty; - packages = with pkgs; [ - # For X11 terminal emulator: - alacritty - - # For glinfo: - mesa-demos - ]; - command = "alacritty"; - enablements = { - wayland = false; - x11 = true; - dbus = false; - pipewire = false; - }; - }; - - "cat.gensokyo.extern.foot.directWayland" = { - name = "da-foot"; - identity = 4; - verbose = true; - insecureWayland = true; - share = pkgs.foot; - packages = with pkgs; [ - foot - - # For wayland-info: - wayland-utils - ]; - command = "foot"; - enablements = { - dbus = false; - pipewire = false; - }; - }; - - "cat.gensokyo.extern.strace.wantFail" = { - name = "strace-failure"; - identity = 5; - verbose = true; - share = pkgs.strace; - command = "strace true"; - enablements = { - wayland = false; - x11 = false; - dbus = false; - pipewire = false; - }; - }; - }; - }; -} diff --git a/test/default.nix b/test/default.nix deleted file mode 100644 index 81daa0a2..00000000 --- a/test/default.nix +++ /dev/null @@ -1,81 +0,0 @@ -{ - lib, - testers, - buildFHSEnv, - writeShellScriptBin, - - system, - self, - withRace ? false, -}: - -testers.nixosTest { - name = "hakurei" + (if withRace then "-race" else ""); - nodes.machine = - { options, pkgs, ... }: - let - fhs = - let - hakurei = options.environment.hakurei.package.default; - in - 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: - (writeShellScriptBin "hakurei-test" '' - # Assert hst CGO_ENABLED=0: ${ - with pkgs; - runCommand "hakurei-hst-cgo" { nativeBuildInputs = [ self.packages.${system}.hakurei.go ]; } '' - cp -r ${options.environment.hakurei.package.default.src} "$out" - chmod -R +w "$out" - cp ${writeText "hst_cgo_test.go" ''package hakurei_test;import("testing";"hakurei.app/hst");func TestTemplate(t *testing.T){hst.Template()}''} "$out/hst_cgo_test.go" - (cd "$out" && HOME="$(mktemp -d)" CGO_ENABLED=0 go test .) - '' - } - - cd ${self.packages.${system}.hakurei.src} - ${fhs}/bin/hakurei-fhs -c \ - 'CC="clang -O3 -Werror" go test --tags=noskip ${if withRace then "-race" else "-count 16"} ./...' \ - &> /tmp/hakurei-test.log && \ - touch /tmp/hakurei-test-ok - touch /tmp/hakurei-test-done - '') - ]; - - # Run with Go race detector: - environment.hakurei = lib.mkIf withRace rec { - # race detector does not support static linking - package = (pkgs.callPackage ./package.nix { }).overrideAttrs (previousAttrs: { - env = previousAttrs.env // { - GOFLAGS = previousAttrs.env.GOFLAGS + " -race"; - }; - }); - hsuPackage = options.environment.hakurei.hsuPackage.default.override { hakurei = package; }; - }; - - 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; -} diff --git a/test/flake.lock b/test/flake.lock deleted file mode 100644 index 5537506a..00000000 --- a/test/flake.lock +++ /dev/null @@ -1,49 +0,0 @@ -{ - "nodes": { - "home-manager": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1787146702, - "narHash": "sha256-YbRcLdU/yK4gWsQg7V8WTKZHfXL33g8+wSFUX3wyevs=", - "owner": "nix-community", - "repo": "home-manager", - "rev": "173b7e8d40fdc8c296a9c99854314f17a3a1704c", - "type": "github" - }, - "original": { - "owner": "nix-community", - "ref": "release-26.05", - "repo": "home-manager", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1787101114, - "narHash": "sha256-gwrPcFf/rDjHPaVflbDZ040ZDmBTRj/7+s8ZmE2SaIM=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "b18a4b905f8d028dc4476412e6d6891728695379", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-26.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "home-manager": "home-manager", - "nixpkgs": "nixpkgs" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/test/flake.nix b/test/flake.nix deleted file mode 100644 index 01a07f78..00000000 --- a/test/flake.nix +++ /dev/null @@ -1,255 +0,0 @@ -{ - description = "hakurei container tool and nixos module"; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; - - home-manager = { - url = "github:nix-community/home-manager/release-26.05"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - }; - - outputs = - { - self, - nixpkgs, - home-manager, - }: - let - supportedSystems = [ - "aarch64-linux" - "i686-linux" - "x86_64-linux" - ]; - - forAllSystems = nixpkgs.lib.genAttrs supportedSystems; - nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; }); - in - { - nixosModules.hakurei = import ./nixos.nix self.packages; - - checks = forAllSystems ( - system: - let - pkgs = nixpkgsFor.${system}; - - inherit (pkgs) - runCommandLocal - callPackage - nixfmt - deadnix - statix - ; - in - { - hakurei = callPackage ./. { inherit system self; }; - race = callPackage ./. { - inherit system self; - withRace = true; - }; - - sandbox = callPackage ./sandbox { inherit self; }; - sandbox-race = callPackage ./sandbox { - inherit self; - withRace = true; - }; - - sharefs = callPackage ./sharefs { inherit system self; }; - - formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt ]; } '' - cd ${./.} - - echo "running nixfmt..." - nixfmt --width=256 --check . - - touch $out - ''; - - lint = - runCommandLocal "check-lint" - { - nativeBuildInputs = [ - deadnix - statix - ]; - } - '' - cd ${./.} - - echo "running deadnix..." - deadnix --fail - - echo "running statix..." - statix check . - - touch $out - ''; - } - ); - - packages = forAllSystems ( - system: - let - inherit (self.packages.${system}) hakurei hsu; - pkgs = nixpkgsFor.${system}; - in - { - default = hakurei; - hakurei = pkgs.pkgsStatic.callPackage ./package.nix { - inherit (pkgs) - # passthru.buildInputs - go_1_27 - clang - - # nativeBuildInputs - pkg-config - wayland-scanner - makeBinaryWrapper - - # appPackages - glibc - xdg-dbus-proxy - - # for check - util-linux - nettools - ; - }; - hsu = pkgs.callPackage ./hsu.nix { inherit (self.packages.${system}) hakurei; }; - sharefs = pkgs.linkFarm "sharefs" { - "bin/sharefs" = "${hakurei}/libexec/sharefs"; - "bin/mount.fuse.sharefs" = "${hakurei}/libexec/sharefs"; - }; - - dist = - pkgs.runCommand "${hakurei.name}-dist" - { - buildInputs = hakurei.targetPkgs ++ [ - pkgs.pkgsStatic.musl - ]; - } - '' - cd $(mktemp -d) \ - && cp -r ${hakurei.src}/. . \ - && chmod +w cmd && cp -r ${hsu.src}/. cmd/hsu/ \ - && chmod -R +w . - - CC="musl-clang -O3 -Werror -Qunused-arguments" \ - GOCACHE="$(mktemp -d)" \ - PATH="${pkgs.pkgsStatic.musl.bin}/bin:$PATH" \ - DESTDIR="$out" \ - ./all.sh - ''; - } - ); - - devShells = forAllSystems ( - system: - let - inherit (self.packages.${system}) hakurei; - pkgs = nixpkgsFor.${system}; - in - { - default = pkgs.mkShell { - buildInputs = hakurei.targetPkgs; - hardeningDisable = [ "fortify" ]; - }; - withPackage = pkgs.mkShell { buildInputs = [ hakurei ] ++ hakurei.targetPkgs; }; - - vm = - let - nixos = nixpkgs.lib.nixosSystem { - inherit system; - modules = [ - { - environment = { - systemPackages = [ - (pkgs.buildFHSEnv { - pname = "hakurei-fhs"; - inherit (hakurei) version; - targetPkgs = _: hakurei.targetPkgs; - extraOutputsToInstall = [ "dev" ]; - profile = '' - export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH" - ''; - }) - ]; - - hakurei = - let - # this is used for interactive vm testing during development, where tests might be broken - package = self.packages.${pkgs.stdenv.hostPlatform.system}.hakurei.override { - buildGo127Module = previousArgs: pkgs.pkgsStatic.buildGo127Module (previousArgs // { doCheck = false; }); - }; - in - { - inherit package; - hsuPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.hsu.override { hakurei = package; }; - }; - }; - } - - ./interactive/configuration.nix - ./interactive/vm.nix - ./interactive/hakurei.nix - ./interactive/trace.nix - ./interactive/raceattr.nix - - self.nixosModules.hakurei - home-manager.nixosModules.home-manager - ]; - }; - in - pkgs.mkShell { - buildInputs = [ nixos.config.system.build.vm ]; - shellHook = "exec run-nixos-vm $@"; - }; - - generateDoc = - let - inherit (pkgs) lib; - - doc = - let - eval = lib.evalModules { - specialArgs = { - inherit pkgs; - }; - modules = [ (import ./options.nix self.packages) ]; - }; - cleanEval = lib.filterAttrsRecursive (n: _: n != "_module") eval; - in - pkgs.nixosOptionsDoc { inherit (cleanEval) options; }; - docText = pkgs.runCommand "hakurei-module-docs.md" { } '' - cat ${doc.optionsCommonMark} > $out - sed -i '/*Declared by:*/,+1 d' $out - ''; - in - pkgs.mkShell { - shellHook = '' - exec cat ${docText} > options.md - ''; - }; - - generateSyscallTable = - let - GOARCH = { - x86_64-linux = "amd64"; - aarch64-linux = "arm64"; - }; - in - pkgs.mkShell { - shellHook = "exec ${pkgs.writeShellScript "generate-syscall-table" '' - set -e - ${pkgs.perl}/bin/perl \ - container/std/mksysnum_linux.pl \ - ${pkgs.linuxHeaders}/include/asm/unistd_64.h | \ - ${pkgs.go}/bin/gofmt > \ - container/std/syscall_linux_${GOARCH.${system}}.go - ''}"; - }; - } - ); - }; -} diff --git a/test/hsu.nix b/test/hsu.nix deleted file mode 100644 index 7acb3529..00000000 --- a/test/hsu.nix +++ /dev/null @@ -1,23 +0,0 @@ -{ - lib, - buildGoModule, - hakurei ? abort "hakurei package required", -}: - -buildGoModule { - pname = "${hakurei.pname}-hsu"; - inherit (hakurei) version; - - src = ../cmd/hsu; - inherit (hakurei) vendorHash; - env.CGO_ENABLED = 0; - - preBuild = '' - go mod init hsu >& /dev/null - ''; - - ldflags = lib.attrsets.foldlAttrs ( - ldflags: name: value: - ldflags ++ [ "-X main.${name}=${value}" ] - ) [ "-s -w" ] { hakureiPath = "${hakurei}/libexec/hakurei"; }; -} diff --git a/test/interactive/configuration.nix b/test/interactive/configuration.nix deleted file mode 100644 index 9d1bddd8..00000000 --- a/test/interactive/configuration.nix +++ /dev/null @@ -1,68 +0,0 @@ -{ pkgs, ... }: -{ - system.stateVersion = "23.05"; - - users.users = { - alice = { - isNormalUser = true; - description = "Alice Foobar"; - password = "foobar"; - uid = 1000; - extraGroups = [ "wheel" ]; - }; - untrusted = { - isNormalUser = true; - description = "Untrusted user"; - password = "foobar"; - uid = 1001; - }; - }; - - home-manager.users.alice.home.stateVersion = "24.11"; - - security = { - sudo.wheelNeedsPassword = false; - rtkit.enable = true; - pam.loginLimits = [ - { - domain = "@users"; - item = "rtprio"; - type = "-"; - value = 1; - } - ]; - }; - - services = { - getty.autologinUser = "alice"; - pipewire = { - enable = true; - alsa.enable = true; - alsa.support32Bit = true; - pulse.enable = true; - jack.enable = true; - }; - }; - - environment.variables = { - SWAYSOCK = "/tmp/sway-ipc.sock"; - WLR_RENDERER = "pixman"; - }; - - programs = { - sway.enable = true; - - 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') > ~/.config/sway/config - - sway --validate - systemd-cat --identifier=session sway && touch /tmp/sway-exit-ok - fi - ''; - }; -} diff --git a/test/interactive/hakurei.nix b/test/interactive/hakurei.nix deleted file mode 100644 index ef5d0ada..00000000 --- a/test/interactive/hakurei.nix +++ /dev/null @@ -1,47 +0,0 @@ -{ pkgs, ... }: -{ - environment.hakurei = rec { - enable = true; - stateDir = "/var/lib/hakurei"; - sharefs.source = "${stateDir}/sdcard"; - users.alice = 0; - apps = { - "cat.gensokyo.extern.foot.noEnablements" = { - name = "ne-foot"; - identity = 1; - shareUid = true; - verbose = true; - share = pkgs.foot; - packages = [ pkgs.foot ]; - command = "foot"; - enablements = { - dbus = false; - pipewire = false; - }; - }; - - "cat.gensokyo.extern.foot.badDaemon" = { - name = "bd-foot"; - identity = 1; - shareUid = true; - verbose = true; - share = pkgs.foot; - packages = [ pkgs.foot ]; - command = "foot"; - enablements = { - dbus = false; - }; - extraPaths = [ - { - type = "daemon"; - dst = "/proc/nonexistent"; - path = "/usr/bin/env"; - args = [ "false" ]; - } - ]; - }; - }; - - extraHomeConfig.home.stateVersion = "23.05"; - }; -} diff --git a/test/interactive/raceattr.nix b/test/interactive/raceattr.nix deleted file mode 100644 index de54f4cf..00000000 --- a/test/interactive/raceattr.nix +++ /dev/null @@ -1,30 +0,0 @@ -{ lib, pkgs, ... }: -{ - security.wrappers.raceattr = - let - inherit (pkgs) buildGoModule; - in - { - setuid = true; - owner = "root"; - group = "root"; - source = "${ - (buildGoModule rec { - name = "raceattr"; - pname = name; - tags = [ "raceattr" ]; - - src = builtins.path { - name = "${pname}-src"; - path = lib.cleanSource ../sharefs; - filter = path: type: (type == "directory") || (type == "regular" && lib.hasSuffix ".go" path); - }; - vendorHash = null; - - preBuild = '' - go mod init hakurei.app/raceattr >& /dev/null - ''; - }) - }/bin/raceattr"; - }; -} diff --git a/test/interactive/trace.nix b/test/interactive/trace.nix deleted file mode 100644 index b9ef872c..00000000 --- a/test/interactive/trace.nix +++ /dev/null @@ -1,35 +0,0 @@ -{ lib, pkgs, ... }: -let - tracing = name: "\"/sys/kernel/debug/tracing/${name}\""; -in -{ - environment.systemPackages = [ - (pkgs.writeShellScriptBin "hakurei-set-up-tracing" '' - set -e - echo "$1" > ${tracing "set_graph_function"} - echo function_graph > ${tracing "current_tracer"} - echo funcgraph-tail > ${tracing "trace_options"} - echo funcgraph-retval > ${tracing "trace_options"} - echo nofuncgraph-cpu > ${tracing "trace_options"} - echo nofuncgraph-overhead > ${tracing "trace_options"} - echo nofuncgraph-duration > ${tracing "trace_options"} - '') - (pkgs.writeShellScriptBin "hakurei-print-trace" "exec cat ${tracing "trace"}") - (pkgs.writeShellScriptBin "hakurei-consume-trace" "exec cat ${tracing "trace_pipe"}") - ]; - - boot.kernelPatches = [ - { - name = "funcgraph-retval"; - patch = null; - structuredExtraConfig = with lib.kernel; { - FUNCTION_GRAPH_RETVAL = yes; - - RUST = lib.mkForce unset; - DRM_NOVA = lib.mkForce unset; - DRM_PANIC_SCREEN_QR_CODE = lib.mkForce unset; - NOVA_CORE = lib.mkForce unset; - }; - } - ]; -} diff --git a/test/interactive/vm.nix b/test/interactive/vm.nix deleted file mode 100644 index 522878e9..00000000 --- a/test/interactive/vm.nix +++ /dev/null @@ -1,56 +0,0 @@ -{ - virtualisation.vmVariant.virtualisation = { - memorySize = 4096; - qemu.options = [ - "-vga none -device virtio-gpu-pci" - "-smp 8" - ]; - - mountHostNixStore = true; - writableStore = true; - writableStoreUseTmpfs = false; - - sharedDirectories = { - cwd = { - target = "/mnt/.ro-cwd"; - source = ''"$OLDPWD"''; - securityModel = "none"; - }; - }; - - fileSystems = { - "/mnt/.ro-cwd".options = [ - "ro" - "noatime" - ]; - "/mnt/cwd".overlay = { - lowerdir = [ "/mnt/.ro-cwd" ]; - upperdir = "/tmp/.cwd/upper"; - workdir = "/tmp/.cwd/work"; - }; - - "/mnt/src".overlay = { - lowerdir = [ ../.. ]; - upperdir = "/tmp/.src/upper"; - workdir = "/tmp/.src/work"; - }; - }; - }; - - systemd.services = { - logrotate-checkconf.enable = false; - hakurei-src-fix-ownership = { - wantedBy = [ "multi-user.target" ]; - wants = [ "mnt-src.mount" ]; - after = [ "mnt-src.mount" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - script = '' - chown -R alice:users /mnt/src/ - chmod -R +w /mnt/src/ - ''; - }; - }; -} diff --git a/test/internal/sandbox/assert.go b/test/internal/sandbox/assert.go deleted file mode 100644 index e6fe926b..00000000 --- a/test/internal/sandbox/assert.go +++ /dev/null @@ -1,272 +0,0 @@ -//go:build testtool - -// Package sandbox provides utilities for checking sandbox outcome. -// -// This package must never be used outside integration tests, there is a much -// better native implementation of mountinfo in the public sandbox/vfs package. -// Files in this package are excluded by the build system to prevent accidental -// misuse. -package sandbox - -import ( - "crypto/sha512" - "encoding/hex" - "encoding/json" - "errors" - "io/fs" - "log" - "net" - "os" - "path/filepath" - "syscall" -) - -var ( - assert = log.New(os.Stderr, "sandbox: ", 0) - printfFunc = assert.Printf - fatalfFunc = assert.Fatalf -) - -func printf(format string, v ...any) { printfFunc(format, v...) } -func fatalf(format string, v ...any) { fatalfFunc(format, v...) } - -type TestCase struct { - Env []string `json:"env"` - FS *FS `json:"fs"` - Mount []*MountinfoEntry `json:"mount"` - Seccomp bool `json:"seccomp"` - - TrySocket string `json:"try_socket,omitempty"` - SocketAbstract bool `json:"socket_abstract,omitempty"` - SocketPathname bool `json:"socket_pathname,omitempty"` -} - -type T struct { - FS fs.FS - - MountsPath string -} - -func (t *T) MustCheckFile(wantFilePath string) { - var want *TestCase - mustDecode(wantFilePath, &want) - t.MustCheck(want) -} - -func mustAbs(s string) string { - if !filepath.IsAbs(s) { - fatalf("[FAIL] %q is not absolute", s) - panic("unreachable") - } - return s -} - -func (t *T) MustCheck(want *TestCase) { - checkWritableDirPaths := []string{ - "/dev/shm", - "/tmp", - os.Getenv("XDG_RUNTIME_DIR"), - } - for _, a := range checkWritableDirPaths { - pathname := filepath.Join(mustAbs(a), ".hakurei-check") - if err := os.WriteFile(pathname, make([]byte, 1<<8), 0600); err != nil { - fatalf("[FAIL] %s", err) - } else if err = os.Remove(pathname); err != nil { - fatalf("[FAIL] %s", err) - } else { - printf("[ OK ] %s is writable", a) - } - } - - if want.Env != nil { - var ( - fail bool - i int - got string - ) - for i, got = range os.Environ() { - if i == len(want.Env) { - fatalf("got more than %d environment variables", len(want.Env)) - } - if got != want.Env[i] { - fail = true - printf("[FAIL] %s", got) - } else { - printf("[ OK ] %s", got) - } - } - - i++ - if i != len(want.Env) { - fatalf("got %d environment variables, want %d", i, len(want.Env)) - } - - if fail { - fatalf("[FAIL] some environment variables did not match") - } - } else { - printf("[SKIP] skipping environ check") - } - - if want.FS != nil && t.FS != nil { - if err := want.FS.Compare(".", t.FS); err != nil { - fatalf("%v", err) - } - } else { - printf("[SKIP] skipping fs check") - } - - if want.Mount != nil { - var fail bool - m := mustParseMountinfo(t.MountsPath) - i := 0 - for ent := range m.Entries() { - if i == len(want.Mount) { - fatalf("got more than %d entries", i) - } - if !ent.EqualWithIgnore(want.Mount[i], "//ignore") { - fail = true - printf("[FAIL] %s", ent) - } else { - printf("[ OK ] %s", ent) - } - - i++ - } - if err := m.Err(); err != nil { - fatalf("%v", err) - } - - if i != len(want.Mount) { - fatalf("got %d entries, want %d", i, len(want.Mount)) - } - - if fail { - fatalf("[FAIL] some mount points did not match") - } - } else { - printf("[SKIP] skipping mounts check") - } - - if want.Seccomp { - if trySyscalls() != nil { - os.Exit(1) - } - } else { - printf("[SKIP] skipping seccomp check") - } - - if want.TrySocket != "" { - abstractConn, abstractErr := net.Dial("unix", "@"+want.TrySocket) - pathnameConn, pathnameErr := net.Dial("unix", want.TrySocket) - ok := true - - if abstractErr == nil { - if err := abstractConn.Close(); err != nil { - ok = false - log.Printf("Close: %v", err) - } - } - if pathnameErr == nil { - if err := pathnameConn.Close(); err != nil { - ok = false - log.Printf("Close: %v", err) - } - } - - abstractWantErr := error(syscall.EPERM) - pathnameWantErr := error(syscall.ENOENT) - if want.SocketAbstract { - abstractWantErr = nil - } - if want.SocketPathname { - pathnameWantErr = nil - } - - if !errors.Is(abstractErr, abstractWantErr) { - ok = false - log.Printf("abstractErr: %v, want %v", abstractErr, abstractWantErr) - } - if !errors.Is(pathnameErr, pathnameWantErr) { - ok = false - log.Printf("pathnameErr: %v, want %v", pathnameErr, pathnameWantErr) - } - - if !ok { - os.Exit(1) - } - } -} - -func MustCheckFilter(pid int, want string) { - err := CheckFilter(pid, want) - if err == nil { - return - } - - perr, ok := errors.AsType[*ptraceError](err) - if !ok { - fatalf("%s", err) - } - switch perr.op { - case "PTRACE_ATTACH": - fatalf("cannot attach to process %d: %v", pid, err) - case "PTRACE_SECCOMP_GET_FILTER": - if perr.errno == syscall.ENOENT { - fatalf("seccomp filter not installed for process %d", pid) - } - fatalf("cannot get filter: %v", err) - default: - fatalf("cannot check filter: %v", err) - } - - *(*int)(nil) = 0 // not reached -} - -func CheckFilter(pid int, want string) error { - if err := ptraceAttach(pid); err != nil { - return err - } - defer func() { - if err := ptraceDetach(pid); err != nil { - printf("cannot detach from process %d: %v", pid, err) - } - }() - - h := sha512.New() - - if buf, err := getFilter[[8]byte](pid, 0); err != nil { - return err - } else { - for _, b := range buf { - h.Write(b[:]) - } - } - - if got := hex.EncodeToString(h.Sum(nil)); got != want { - printf("[FAIL] %s", got) - return syscall.ENOTRECOVERABLE - } else { - printf("[ OK ] %s", got) - return nil - } -} - -func mustDecode(wantFilePath string, v any) { - if f, err := os.Open(wantFilePath); err != nil { - fatalf("cannot open %q: %v", wantFilePath, err) - } else if err = json.NewDecoder(f).Decode(v); err != nil { - fatalf("cannot decode %q: %v", wantFilePath, err) - } else if err = f.Close(); err != nil { - fatalf("cannot close %q: %v", wantFilePath, err) - } -} - -func mustParseMountinfo(name string) *Mountinfo { - m := NewMountinfo(name) - if err := m.Parse(); err != nil { - fatalf("%v", err) - panic("unreachable") - } - return m -} diff --git a/test/internal/sandbox/assert_test.go b/test/internal/sandbox/assert_test.go deleted file mode 100644 index 012ae23d..00000000 --- a/test/internal/sandbox/assert_test.go +++ /dev/null @@ -1,34 +0,0 @@ -//go:build testtool - -package sandbox - -import ( - "encoding/json" - "os" - "path/filepath" - "testing" -) - -type F func(format string, v ...any) - -func SwapPrint(f F) (old F) { old = printfFunc; printfFunc = f; return } -func SwapFatal(f F) (old F) { old = fatalfFunc; fatalfFunc = f; return } - -func MustWantFile(t *testing.T, v any) (wantFile string) { - wantFile = filepath.Join(t.TempDir(), "want.json") - if f, err := os.OpenFile(wantFile, os.O_CREATE|os.O_WRONLY, 0400); err != nil { - t.Fatalf("cannot create %q: %v", wantFile, err) - } else if err = json.NewEncoder(f).Encode(v); err != nil { - t.Fatalf("cannot encode to %q: %v", wantFile, err) - } else if err = f.Close(); err != nil { - t.Fatalf("cannot close %q: %v", wantFile, err) - } - - t.Cleanup(func() { - if err := os.Remove(wantFile); err != nil { - t.Fatalf("cannot remove %q: %v", wantFile, err) - } - }) - - return -} diff --git a/test/internal/sandbox/fs.go b/test/internal/sandbox/fs.go deleted file mode 100644 index b37d30b9..00000000 --- a/test/internal/sandbox/fs.go +++ /dev/null @@ -1,102 +0,0 @@ -//go:build testtool - -package sandbox - -import ( - "errors" - "fmt" - "io/fs" - "path/filepath" - "strings" -) - -var ( - ErrFSBadLength = errors.New("bad dir length") - ErrFSBadData = errors.New("data differs") - ErrFSBadMode = errors.New("mode differs") - ErrFSInvalidEnt = errors.New("invalid entry condition") -) - -type FS struct { - Mode fs.FileMode `json:"mode"` - Dir map[string]*FS `json:"dir"` - Data *string `json:"data"` -} - -func printDir(prefix string, dir []fs.DirEntry) { - names := make([]string, len(dir)) - for i, ent := range dir { - name := ent.Name() - if ent.IsDir() { - name += "/" - } - names[i] = fmt.Sprintf("%q", name) - } - printf("[FAIL] d %s: %s", prefix, strings.Join(names, " ")) -} - -func (s *FS) Compare(prefix string, e fs.FS) error { - if s.Data != nil { - if s.Dir != nil { - panic("invalid state") - } - panic("invalid compare call") - } - - if s.Dir == nil { - printf("[ OK ] s %s", prefix) - return nil - } - - var dir []fs.DirEntry - if d, err := fs.ReadDir(e, prefix); err != nil { - return err - } else if len(d) != len(s.Dir) { - printDir(prefix, d) - return ErrFSBadLength - } else { - dir = d - } - - for _, got := range dir { - name := got.Name() - - if want, ok := s.Dir[name]; !ok { - printDir(prefix, dir) - return fs.ErrNotExist - } else if want.Dir != nil && !got.IsDir() { - printDir(prefix, dir) - return ErrFSInvalidEnt - } else { - name = filepath.Join(prefix, name) - - if fi, err := got.Info(); err != nil { - return err - } else if fi.Mode() != want.Mode { - printf("[FAIL] m %s: %x, want %x", - name, uint32(fi.Mode()), uint32(want.Mode)) - return ErrFSBadMode - } - - if want.Data != nil { - if want.Dir != nil { - panic("invalid state") - } - if v, err := fs.ReadFile(e, name); err != nil { - return err - } else if string(v) != *want.Data { - printf("[FAIL] f %s", name) - printf("got: %s", v) - printf("want: %s", *want.Data) - return ErrFSBadData - } - printf("[ OK ] f %s", name) - } else if err := want.Compare(name, e); err != nil { - return err - } - } - } - printf("[ OK ] d %s", prefix) - - return nil -} diff --git a/test/internal/sandbox/fs_test.go b/test/internal/sandbox/fs_test.go deleted file mode 100644 index a7813ee5..00000000 --- a/test/internal/sandbox/fs_test.go +++ /dev/null @@ -1,80 +0,0 @@ -//go:build testtool - -package sandbox_test - -import ( - "errors" - "fmt" - "io/fs" - "strings" - "testing" - "testing/fstest" - - "hakurei.app/test/internal/sandbox" -) - -var ( - fsPasswdSample = "u0_a20:x:65534:65534:Hakurei:/var/lib/persist/module/hakurei/u0/a20:/run/current-system/sw/bin/zsh" - fsGroupSample = "hakurei:x:65534:" -) - -func TestCompare(t *testing.T) { - testCases := []struct { - name string - - sample fstest.MapFS - want *sandbox.FS - wantOut string - wantErr error - }{ - {"skip", fstest.MapFS{}, &sandbox.FS{}, "[ OK ] s .\x00", nil}, - {"simple pass", fstest.MapFS{".hakurei": {Mode: 0x800001ed}}, - &sandbox.FS{Dir: map[string]*sandbox.FS{".hakurei": {Mode: 0x800001ed}}}, - "[ OK ] s .hakurei\x00[ OK ] d .\x00", nil}, - {"bad length", fstest.MapFS{".hakurei": {Mode: 0x800001ed}}, - &sandbox.FS{Dir: make(map[string]*sandbox.FS)}, - "[FAIL] d .: \".hakurei/\"\x00", sandbox.ErrFSBadLength}, - {"top level bad mode", fstest.MapFS{".hakurei": {Mode: 0x800001ed}}, - &sandbox.FS{Dir: map[string]*sandbox.FS{".hakurei": {Mode: 0xdeadbeef}}}, - "[FAIL] m .hakurei: 800001ed, want deadbeef\x00", sandbox.ErrFSBadMode}, - {"invalid entry condition", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}}, - &sandbox.FS{Dir: map[string]*sandbox.FS{"test": {Dir: make(map[string]*sandbox.FS)}}}, - "[FAIL] d .: \"test\"\x00", sandbox.ErrFSInvalidEnt}, - {"nonexistent", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}}, - &sandbox.FS{Dir: map[string]*sandbox.FS{".test": {}}}, - "[FAIL] d .: \"test\"\x00", fs.ErrNotExist}, - {"file", fstest.MapFS{"etc": {Mode: 0x800001c0}, - "etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644}, - "etc/group": {Data: []byte(fsGroupSample), Mode: 0644}, - }, &sandbox.FS{Dir: map[string]*sandbox.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*sandbox.FS{ - "passwd": {Mode: 0x1a4, Data: &fsPasswdSample}, - "group": {Mode: 0x1a4, Data: &fsGroupSample}, - }}}}, "[ OK ] f etc/group\x00[ OK ] f etc/passwd\x00[ OK ] d etc\x00[ OK ] d .\x00", nil}, - {"file differ", fstest.MapFS{"etc": {Mode: 0x800001c0}, - "etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644}, - "etc/group": {Data: []byte(fsGroupSample), Mode: 0644}, - }, &sandbox.FS{Dir: map[string]*sandbox.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*sandbox.FS{ - "passwd": {Mode: 0x1a4, Data: &fsGroupSample}, - "group": {Mode: 0x1a4, Data: &fsGroupSample}, - }}}}, "[ OK ] f etc/group\x00[FAIL] f etc/passwd\x00got: u0_a20:x:65534:65534:Hakurei:/var/lib/persist/module/hakurei/u0/a20:/run/current-system/sw/bin/zsh\x00want: hakurei:x:65534:\x00", sandbox.ErrFSBadData}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - gotOut := new(strings.Builder) - oldPrint := sandbox.SwapPrint(func(format string, v ...any) { _, _ = fmt.Fprintf(gotOut, format+"\x00", v...) }) - t.Cleanup(func() { sandbox.SwapPrint(oldPrint) }) - - err := tc.want.Compare(".", tc.sample) - if !errors.Is(err, tc.wantErr) { - t.Errorf("Compare: error = %v; wantErr %v", - err, tc.wantErr) - } - - if gotOut.String() != tc.wantOut { - t.Errorf("Compare: output %q; want %q", - gotOut, tc.wantOut) - } - }) - } -} diff --git a/test/internal/sandbox/mount.go b/test/internal/sandbox/mount.go deleted file mode 100644 index 791a68b9..00000000 --- a/test/internal/sandbox/mount.go +++ /dev/null @@ -1,159 +0,0 @@ -//go:build testtool - -package sandbox - -/* -#cgo linux pkg-config: --static mount - -#include <stdlib.h> -#include <stdio.h> -#include <libmount.h> - -const char *HAKUREI_MOUNTINFO_PATH = "/proc/self/mountinfo"; -*/ -import "C" - -import ( - "errors" - "fmt" - "iter" - "runtime" - "sync" - "unsafe" -) - -var ( - ErrMountinfoParse = errors.New("invalid mountinfo records") - ErrMountinfoIter = errors.New("cannot allocate iterator") - ErrMountinfoFault = errors.New("cannot iterate on filesystems") -) - -type ( - Mountinfo struct { - mu sync.RWMutex - p string - err error - - tb *C.struct_libmnt_table - itr *C.struct_libmnt_iter - - fs *C.struct_libmnt_fs - } - - // MountinfoEntry represents deterministic mountinfo parts of a libmnt_fs entry. - MountinfoEntry struct { - // mount ID: a unique ID for the mount (may be reused after umount(2)). - ID int `json:"id"` - // parent ID: the ID of the parent mount (or of self for the root of this mount namespace's mount tree). - Parent int `json:"parent"` - // root: the pathname of the directory in the filesystem which forms the root of this mount. - Root string `json:"root"` - // mount point: the pathname of the mount point relative to the process's root directory. - Target string `json:"target"` - // mount options: per-mount options (see mount(2)). - VfsOptstr string `json:"vfs_optstr"` - // filesystem type: the filesystem type in the form "type[.subtype]". - FsType string `json:"fstype"` - // mount source: filesystem-specific information or "none". - Source string `json:"source"` - // super options: per-superblock options (see mount(2)). - FsOptstr string `json:"fs_optstr"` - } -) - -func (m *Mountinfo) copy(v *MountinfoEntry) { - if m.fs == nil { - panic("invalid entry") - } - v.ID = int(C.mnt_fs_get_id(m.fs)) - v.Parent = int(C.mnt_fs_get_parent_id(m.fs)) - v.Root = C.GoString(C.mnt_fs_get_root(m.fs)) - v.Target = C.GoString(C.mnt_fs_get_target(m.fs)) - v.VfsOptstr = C.GoString(C.mnt_fs_get_vfs_options(m.fs)) - v.FsType = C.GoString(C.mnt_fs_get_fstype(m.fs)) - v.Source = C.GoString(C.mnt_fs_get_source(m.fs)) - v.FsOptstr = C.GoString(C.mnt_fs_get_fs_options(m.fs)) -} - -func NewMountinfo(p string) *Mountinfo { m := new(Mountinfo); m.p = p; return m } - -func (m *Mountinfo) Err() error { m.mu.RLock(); defer m.mu.RUnlock(); return m.err } - -func (m *Mountinfo) Parse() error { - m.mu.Lock() - defer m.mu.Unlock() - - if m.tb != nil { - panic("open called twice") - } - - if m.p == "" { - m.tb = C.mnt_new_table_from_file(C.HAKUREI_MOUNTINFO_PATH) - } else { - name := C.CString(m.p) - m.tb = C.mnt_new_table_from_file(name) - C.free(unsafe.Pointer(name)) - } - if m.tb == nil { - return ErrMountinfoParse - } - m.itr = C.mnt_new_iter(C.MNT_ITER_FORWARD) - if m.itr == nil { - C.mnt_unref_table(m.tb) - return ErrMountinfoIter - } - - runtime.SetFinalizer(m, (*Mountinfo).Unref) - return nil -} - -func (m *Mountinfo) Unref() { - m.mu.Lock() - defer m.mu.Unlock() - - if m.tb == nil { - panic("unref called before parse") - } - - C.mnt_unref_table(m.tb) - C.mnt_free_iter(m.itr) - runtime.SetFinalizer(m, nil) -} - -func (m *Mountinfo) Entries() iter.Seq[*MountinfoEntry] { - return func(yield func(*MountinfoEntry) bool) { - m.mu.Lock() - defer m.mu.Unlock() - - C.mnt_reset_iter(m.itr, -1) - - var rc C.int - ent := new(MountinfoEntry) - for rc = C.mnt_table_next_fs(m.tb, m.itr, &m.fs); rc == 0; rc = C.mnt_table_next_fs(m.tb, m.itr, &m.fs) { - m.copy(ent) - if !yield(ent) { - return - } - } - if rc < 0 { - m.err = ErrMountinfoFault - return - } - } -} - -func (e *MountinfoEntry) EqualWithIgnore(want *MountinfoEntry, ignore string) bool { - return (e.ID == want.ID || want.ID == -1) && - (e.Parent == want.Parent || want.Parent == -1) && - (e.Root == want.Root || want.Root == ignore) && - (e.Target == want.Target || want.Target == ignore) && - (e.VfsOptstr == want.VfsOptstr || want.VfsOptstr == ignore) && - (e.FsType == want.FsType || want.FsType == ignore) && - (e.Source == want.Source || want.Source == ignore) && - (e.FsOptstr == want.FsOptstr || want.FsOptstr == ignore) -} - -func (e *MountinfoEntry) String() string { - return fmt.Sprintf("%d %d %s %s %s %s %s %s", - e.ID, e.Parent, e.Root, e.Target, e.VfsOptstr, e.FsType, e.Source, e.FsOptstr) -} diff --git a/test/internal/sandbox/ptrace.go b/test/internal/sandbox/ptrace.go deleted file mode 100644 index 8272ff5b..00000000 --- a/test/internal/sandbox/ptrace.go +++ /dev/null @@ -1,87 +0,0 @@ -//go:build testtool - -package sandbox - -import ( - "errors" - "fmt" - "syscall" - "unsafe" -) - -const ( - NULL = 0 - - PTRACE_ATTACH = 16 - PTRACE_DETACH = 17 - PTRACE_SECCOMP_GET_FILTER = 0x420c -) - -type ptraceError struct { - op string - errno syscall.Errno -} - -func (p *ptraceError) Error() string { return fmt.Sprintf("%s: %v", p.op, p.errno) } - -func (p *ptraceError) Unwrap() error { - if p.errno == 0 { - return nil - } - return p.errno -} - -func ptrace(op uintptr, pid, addr int, data unsafe.Pointer) (r uintptr, errno syscall.Errno) { - r, _, errno = syscall.Syscall6(syscall.SYS_PTRACE, op, uintptr(pid), uintptr(addr), uintptr(data), NULL, NULL) - return -} - -func ptraceAttach(pid int) error { - if _, errno := ptrace(PTRACE_ATTACH, pid, 0, nil); errno != 0 { - return &ptraceError{"PTRACE_ATTACH", errno} - } - - var status syscall.WaitStatus - for { - if _, err := syscall.Wait4(pid, &status, syscall.WALL, nil); err != nil { - if errors.Is(err, syscall.EINTR) { - continue - } - fatalf("cannot waitpid: %v", err) - } - break - } - - return nil -} - -func ptraceDetach(pid int) error { - if _, errno := ptrace(PTRACE_DETACH, pid, 0, nil); errno != 0 { - return &ptraceError{"PTRACE_DETACH", errno} - } - return nil -} - -type sockFilter struct { /* Filter block */ - code uint16 /* Actual filter code */ - jt uint8 /* Jump true */ - jf uint8 /* Jump false */ - k uint32 /* Generic multiuse field */ -} - -func getFilter[T comparable](pid, index int) ([]T, error) { - if s := unsafe.Sizeof(*new(T)); s != 8 { - panic(fmt.Sprintf("invalid filter block size %d", s)) - } - - var buf []T - if n, errno := ptrace(PTRACE_SECCOMP_GET_FILTER, pid, index, nil); errno != 0 { - return nil, &ptraceError{"PTRACE_SECCOMP_GET_FILTER", errno} - } else { - buf = make([]T, n) - } - if _, errno := ptrace(PTRACE_SECCOMP_GET_FILTER, pid, index, unsafe.Pointer(&buf[0])); errno != 0 { - return nil, &ptraceError{"PTRACE_SECCOMP_GET_FILTER", errno} - } - return buf, nil -} diff --git a/test/internal/sandbox/seccomp.go b/test/internal/sandbox/seccomp.go deleted file mode 100644 index 7df781d2..00000000 --- a/test/internal/sandbox/seccomp.go +++ /dev/null @@ -1,44 +0,0 @@ -//go:build testtool - -package sandbox - -import ( - "os" - "syscall" -) - -/* -#include <sys/quota.h> -*/ -import "C" - -func trySyscalls() error { - testCases := []struct { - name string - errno syscall.Errno - - trap, a1, a2, a3, a4, a5, a6 uintptr - }{ - {"syslog", syscall.EPERM, syscall.SYS_SYSLOG, 0, NULL, NULL, NULL, NULL, NULL}, - {"acct", syscall.EPERM, syscall.SYS_ACCT, 0, NULL, NULL, NULL, NULL, NULL}, - {"quotactl", syscall.EPERM, syscall.SYS_QUOTACTL, C.Q_GETQUOTA, NULL, uintptr(os.Getuid()), NULL, NULL, NULL}, - {"add_key", syscall.EPERM, syscall.SYS_ADD_KEY, NULL, NULL, NULL, NULL, NULL, NULL}, - {"keyctl", syscall.EPERM, syscall.SYS_KEYCTL, NULL, NULL, NULL, NULL, NULL, NULL}, - {"request_key", syscall.EPERM, syscall.SYS_REQUEST_KEY, NULL, NULL, NULL, NULL, NULL, NULL}, - {"move_pages", syscall.EPERM, syscall.SYS_MOVE_PAGES, uintptr(os.Getpid()), NULL, NULL, NULL, NULL, NULL}, - {"mbind", syscall.EPERM, syscall.SYS_MBIND, NULL, NULL, NULL, NULL, NULL, NULL}, - {"get_mempolicy", syscall.EPERM, syscall.SYS_GET_MEMPOLICY, NULL, NULL, NULL, NULL, NULL, NULL}, - {"set_mempolicy", syscall.EPERM, syscall.SYS_SET_MEMPOLICY, NULL, NULL, NULL, NULL, NULL, NULL}, - {"migrate_pages", syscall.EPERM, syscall.SYS_MIGRATE_PAGES, NULL, NULL, NULL, NULL, NULL, NULL}, - } - - for _, tc := range testCases { - if _, _, errno := syscall.Syscall6(tc.trap, tc.a1, tc.a2, tc.a3, tc.a4, tc.a5, tc.a6); errno != tc.errno { - printf("[FAIL] %s: %v, want %v", tc.name, errno, tc.errno) - return errno - } - printf("[ OK ] %s: %v", tc.name, tc.errno) - } - - return nil -} diff --git a/test/nixos.nix b/test/nixos.nix deleted file mode 100644 index 49bfffb6..00000000 --- a/test/nixos.nix +++ /dev/null @@ -1,407 +0,0 @@ -packages: -{ - lib, - pkgs, - config, - ... -}: - -let - inherit (lib) - lists - attrsets - mkMerge - mkIf - mapAttrs - foldlAttrs - optional - optionals - ; - - cfg = config.environment.hakurei; - - # userid*userOffset + appStart + appid - getsubuid = userid: appid: userid * 100000 + 10000 + appid; - getsubname = userid: appid: "u${toString userid}_a${toString appid}"; - getsubhome = userid: appid: "${cfg.stateDir}/u${toString userid}/a${toString appid}"; - - mountpoints = { - ${cfg.sharefs.name} = mkIf (cfg.sharefs.source != null) { - depends = [ cfg.sharefs.source ]; - device = "sharefs"; - fsType = "fuse.sharefs"; - noCheck = true; - options = [ - "rw" - "noexec" - "nosuid" - "nodev" - "noatime" - "allow_other" - "mkdir" - "source=${cfg.sharefs.source}" - "setuid=${toString config.users.users.${cfg.sharefs.user}.uid}" - "setgid=${toString config.users.groups.${cfg.sharefs.group}.gid}" - ]; - }; - }; -in - -{ - imports = [ (import ./options.nix packages) ]; - - options = { - # Forward declare a dummy option for VM filesystems since the real one won't exist - # unless the VM module is actually imported. - virtualisation.fileSystems = lib.mkOption { }; - }; - - config = mkIf cfg.enable { - assertions = [ - ( - let - conflictingApps = foldlAttrs ( - acc: id: app: - ( - acc - ++ foldlAttrs ( - acc': id': app': - if id == id' || app.shareUid && app'.shareUid || app.identity != app'.identity then acc' else acc' ++ [ id ] - ) [ ] cfg.apps - ) - ) [ ] cfg.apps; - in - { - assertion = (lists.length conflictingApps) == 0; - message = "the following hakurei apps have conflicting identities: " + (builtins.concatStringsSep ", " conflictingApps); - } - ) - ]; - - security.wrappers.hsu = { - source = "${cfg.hsuPackage}/bin/hsu"; - setuid = true; - owner = "root"; - group = "root"; - }; - - environment.etc.hsurc = { - mode = "0400"; - text = foldlAttrs ( - acc: username: fid: - "${toString config.users.users.${username}.uid} ${toString fid}\n" + acc - ) "" cfg.users; - }; - - environment.systemPackages = optional (cfg.sharefs.source != null) cfg.sharefs.package; - fileSystems = mountpoints; - virtualisation.fileSystems = mountpoints; - - home-manager = - let - privPackages = mapAttrs (_: userid: { - home.packages = foldlAttrs ( - acc: id: app: - [ - ( - let - extendDBusDefault = id: ext: { - filter = true; - - talk = [ "org.freedesktop.Notifications" ] ++ ext.talk; - own = [ - "${id}.*" - "org.mpris.MediaPlayer2.${id}.*" - ] - ++ ext.own; - - inherit (ext) call broadcast; - }; - dbusConfig = - let - default = { - talk = [ ]; - own = [ ]; - call = { }; - broadcast = { }; - }; - in - { - session_bus = if app.dbus.session != null then (app.dbus.session (extendDBusDefault id)) else (extendDBusDefault id default); - system_bus = app.dbus.system; - }; - command = if app.command == null then app.name else app.command; - script = if app.script == null then ("exec " + command + " $@") else app.script; - isGraphical = if app.gpu != null then app.gpu else app.enablements.wayland || app.enablements.x11; - - conf = { - inherit id; - inherit (app) identity enablements; - inherit (dbusConfig) session_bus system_bus; - direct_wayland = app.insecureWayland; - sched_policy = app.schedPolicy; - sched_priority = app.schedPriority; - groups = app.groups ++ optional (cfg.sharefs.source != null) cfg.sharefs.group; - - container = { - inherit (app) - wait_delay - devel - userns - device - tty - multiarch - env - ; - map_real_uid = app.mapRealUid; - host_net = app.hostNet; - host_abstract = app.hostAbstract; - share_runtime = app.shareRuntime; - share_tmpdir = app.shareTmpdir; - - filesystem = - let - bind = src: { - type = "bind"; - inherit src; - }; - optBind = src: { - type = "bind"; - inherit src; - optional = true; - }; - optDevBind = src: { - type = "bind"; - inherit src; - dev = true; - optional = true; - }; - in - [ - (bind "/bin") - (bind "/usr/bin") - (bind "/nix/store") - (optBind "/sys/block") - (optBind "/sys/bus") - (optBind "/sys/class") - (optBind "/sys/dev") - (optBind "/sys/devices") - ] - ++ optionals app.nix [ - (bind "/nix/var") - ] - ++ optionals isGraphical [ - (optDevBind "/dev/dri") - (optDevBind "/dev/nvidiactl") - (optDevBind "/dev/nvidia-modeset") - (optDevBind "/dev/nvidia-uvm") - (optDevBind "/dev/nvidia-uvm-tools") - (optDevBind "/dev/nvidia0") - ] - ++ optionals app.useCommonPaths cfg.commonPaths - ++ app.extraPaths - ++ [ - { - type = "bind"; - dst = "/etc/"; - src = "/etc/"; - special = true; - } - { - type = "link"; - dst = "/run/current-system"; - linkname = "/run/current-system"; - dereference = true; - } - ] - ++ optionals (isGraphical && config.hardware.graphics.enable) ( - [ - { - type = "link"; - dst = "/run/opengl-driver"; - linkname = config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver"."L+".argument; - } - ] - ++ optionals (app.multiarch && config.hardware.graphics.enable32Bit) [ - { - type = "link"; - dst = "/run/opengl-driver-32"; - linkname = config.systemd.tmpfiles.settings.graphics-driver."/run/opengl-driver-32"."L+".argument; - } - ] - ) - ++ [ - { - type = "bind"; - src = getsubhome userid app.identity; - write = true; - ensure = true; - } - ]; - - username = getsubname userid app.identity; - inherit (cfg) shell; - home = getsubhome userid app.identity; - - path = - if app.path == null then - pkgs.writeScript "${app.name}-start" '' - #!${pkgs.zsh}${pkgs.zsh.shellPath} - ${script} - '' - else - app.path; - args = if app.args == null then [ "${app.name}-start" ] else app.args; - }; - }; - - checkedConfig = - name: value: - let - file = pkgs.writeText name (builtins.toJSON value); - in - pkgs.runCommand "checked-${name}" { nativeBuildInputs = [ cfg.package ]; } '' - ln -vs ${file} "$out" - hakurei show --no-store ${file} - ''; - in - pkgs.writeShellScriptBin app.name '' - exec hakurei${if app.verbose then " -v" else ""}${if app.insecureWayland then " --insecure" else ""} run ${checkedConfig "hakurei-app-${app.name}.json" conf} $@ - '' - ) - ] - ++ ( - let - pkg = if app.share != null then app.share else pkgs.${app.name}; - copy = source: "[ -d '${source}' ] && cp -Lrv '${source}' $out/share || true"; - in - optional (app.enablements.wayland || app.enablements.x11) ( - pkgs.runCommand "${app.name}-share" { } '' - mkdir -p $out/share - ${copy "${pkg}/share/applications"} - ${copy "${pkg}/share/pixmaps"} - ${copy "${pkg}/share/icons"} - ${copy "${pkg}/share/man"} - - if test -d "$out/share/applications"; then - substituteInPlace $out/share/applications/* \ - --replace-warn '${pkg}/bin/' "" \ - --replace-warn '${pkg}/libexec/' "" - fi - '' - ) - ) - ++ acc - ) [ cfg.package ] cfg.apps; - }) cfg.users; - in - { - useUserPackages = false; # prevent users.users entries from being added - - users = - mkMerge - (foldlAttrs - ( - acc: _: fid: - foldlAttrs - ( - acc: _: app: - ( - let - key = getsubname fid app.identity; - in - { - usernames = acc.usernames // { - ${key} = true; - }; - merge = acc.merge ++ [ - { - ${key} = mkMerge ( - [ - app.extraConfig - { home.packages = app.packages; } - ] - ++ lib.optional (!attrsets.hasAttrByPath [ key ] acc.usernames) cfg.extraHomeConfig - ); - } - ]; - } - ) - ) - { - inherit (acc) usernames; - merge = acc.merge ++ [ { ${getsubname fid 0} = cfg.extraHomeConfig; } ]; - } - cfg.apps - ) - { - usernames = { }; - merge = [ privPackages ]; - } - cfg.users - ).merge; - }; - - users = - let - getuser = userid: appid: { - isSystemUser = true; - createHome = true; - description = "Hakurei subordinate user ${toString appid} (u${toString userid})"; - group = getsubname userid appid; - home = getsubhome userid appid; - uid = getsubuid userid appid; - }; - getgroup = userid: appid: { gid = getsubuid userid appid; }; - in - { - users = mkMerge ( - foldlAttrs - ( - acc: username: fid: - acc - ++ - foldlAttrs - ( - acc': _: app: - acc' ++ [ { ${getsubname fid app.identity} = getuser fid app.identity; } ] - ) - [ - { - ${getsubname fid 0} = getuser fid 0; - ${username}.extraGroups = [ cfg.sharefs.group ]; - } - ] - cfg.apps - ) - (optional (cfg.sharefs.source != null) { - ${cfg.sharefs.user} = { - uid = lib.mkDefault 1023; - inherit (cfg.sharefs) group; - isSystemUser = true; - home = cfg.sharefs.source; - }; - }) - cfg.users - ); - - groups = mkMerge ( - foldlAttrs - ( - acc: _: fid: - acc - ++ foldlAttrs ( - acc': _: app: - acc' ++ [ { ${getsubname fid app.identity} = getgroup fid app.identity; } ] - ) [ { ${getsubname fid 0} = getgroup fid 0; } ] cfg.apps - ) - (optional (cfg.sharefs.source != null) { - ${cfg.sharefs.group} = { - gid = lib.mkDefault 1023; - }; - }) - cfg.users - ); - }; - }; -} diff --git a/test/options.md b/test/options.md deleted file mode 100644 index 6f6acddd..00000000 --- a/test/options.md +++ /dev/null @@ -1,1175 +0,0 @@ -## environment\.hakurei\.enable - - - -Whether to enable hakurei\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.package - - - -The hakurei package to use\. - - - -*Type:* -package - - - -*Default:* - -```nix -<derivation hakurei-static-x86_64-unknown-linux-musl-0.4.3> -``` - - - -## environment\.hakurei\.apps - -Declaratively configured hakurei apps\. - - - -*Type:* -attribute set of (submodule) - - - -*Default:* - -```nix -{ } -``` - - - -## environment\.hakurei\.apps\.\<name>\.enablements\.dbus - - - -Whether to proxy D-Bus\. - - - -*Type:* -null or boolean - - - -*Default:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\<name>\.enablements\.pipewire - - - -Whether to share the PipeWire server via pipewire-pulse on a SecurityContext socket\. - - - -*Type:* -null or boolean - - - -*Default:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\<name>\.enablements\.wayland - - - -Whether to share the Wayland server via security-context-v1\. - - - -*Type:* -null or boolean - - - -*Default:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\<name>\.enablements\.x11 - - - -Whether to share the X11 socket and allow connection\. - - - -*Type:* -null or boolean - - - -*Default:* - -```nix -false -``` - - - -## environment\.hakurei\.apps\.\<name>\.packages - - - -List of extra packages to install via home-manager\. - - - -*Type:* -list of package - - - -*Default:* - -```nix -[ ] -``` - - - -## environment\.hakurei\.apps\.\<name>\.args - - - -Custom args\. -Setting this to null will default to script name\. - - - -*Type:* -null or (list of string) - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\<name>\.command - - - -Command to run as the target user\. -Setting this to null will default command to launcher name\. -Has no effect when script is set\. - - - -*Type:* -null or string - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\<name>\.dbus\.session - - - -D-Bus session bus custom configuration\. -Setting this to null will enable built-in defaults\. - - - -*Type:* -null or (function that evaluates to a(n) anything) - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\<name>\.dbus\.system - - - -D-Bus system bus custom configuration\. -Setting this to null will disable the system bus proxy\. - - - -*Type:* -null or anything - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\<name>\.devel - - - -Whether to enable debugging-related kernel interfaces\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\<name>\.device - - - -Whether to enable access to all devices\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\<name>\.env - - - -Environment variables to set for the initial process in the sandbox\. - - - -*Type:* -null or (attribute set of string) - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\<name>\.extraConfig - - - -Extra home-manager configuration\. - - - -*Type:* -anything - - - -*Default:* - -```nix -{ } -``` - - - -## environment\.hakurei\.apps\.\<name>\.extraPaths - - - -Extra paths to make available to the container\. - - - -*Type:* -list of attribute set of anything - - - -*Default:* - -```nix -[ ] -``` - - - -## environment\.hakurei\.apps\.\<name>\.gpu - - - -Target process GPU and driver access\. -Setting this to null will enable GPU whenever X or Wayland is enabled\. - - - -*Type:* -null or boolean - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\<name>\.groups - - - -List of groups to inherit from the privileged user\. - - - -*Type:* -list of string - - - -*Default:* - -```nix -[ ] -``` - - - -## environment\.hakurei\.apps\.\<name>\.hostAbstract - - - -Whether to enable share abstract unix socket scope\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\<name>\.hostNet - - - -Whether to enable share host net namespace\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -true -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\<name>\.identity - - - -Application identity\. Identity 0 is reserved for system services\. - - - -*Type:* -integer between 1 and 9999 (both inclusive) - - - -## environment\.hakurei\.apps\.\<name>\.insecureWayland - - - -Whether to enable direct access to the Wayland socket\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\<name>\.mapRealUid - - - -Whether to enable mapping to priv-user uid\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\<name>\.multiarch - - - -Whether to enable multiarch kernel-level support\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\<name>\.name - - - -Name of the app’s launcher script\. - - - -*Type:* -string - - - -## environment\.hakurei\.apps\.\<name>\.nix - - - -Whether to enable nix daemon access\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\<name>\.path - - - -Custom executable path\. -Setting this to null will default to the start script\. - - - -*Type:* -null or string - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\<name>\.schedPolicy - - - -Scheduling policy to set for the container\. -The zero value retains the current scheduling policy\. - - - -*Type:* -null or one of “fifo”, “rr”, “batch”, “idle”, “deadline”, “ext” - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\<name>\.schedPriority - - - -Scheduling priority to set for the container\. - - - -*Type:* -null or integer between 1 and 99 (both inclusive) - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\<name>\.script - - - -Application launch script\. - - - -*Type:* -null or string - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\<name>\.share - - - -Package containing share files\. -Setting this to null will default package name to wrapper name\. - - - -*Type:* -null or package - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.apps\.\<name>\.shareRuntime - - - -Whether to enable sharing of XDG_RUNTIME_DIR between containers under the same identity\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\<name>\.shareTmpdir - - - -Whether to enable sharing of TMPDIR between containers under the same identity\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\<name>\.shareUid - - - -Whether to enable sharing identity with another application\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\<name>\.tty - - - -Whether to enable access to the controlling terminal\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\<name>\.useCommonPaths - - - -Whether to enable common extra paths\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -true -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\<name>\.userns - - - -Whether to enable user namespace creation\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\<name>\.verbose - - - -Whether to enable launchers with verbose output\. - - - -*Type:* -boolean - - - -*Default:* - -```nix -false -``` - - - -*Example:* - -```nix -true -``` - - - -## environment\.hakurei\.apps\.\<name>\.wait_delay - - - -Duration to wait for after interrupting a container’s initial process in nanoseconds\. -A negative value causes the container to be terminated immediately on cancellation\. -Setting this to null defaults to five seconds\. - - - -*Type:* -null or signed integer - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.commonPaths - - - -Common extra paths to make available to the container\. - - - -*Type:* -list of attribute set of anything - - - -*Default:* - -```nix -[ ] -``` - - - -## environment\.hakurei\.extraHomeConfig - - - -Extra home-manager configuration to merge with all target users\. - - - -*Type:* -anything - - - -## environment\.hakurei\.hsuPackage - - - -The hsu package to use\. - - - -*Type:* -package - - - -*Default:* - -```nix -<derivation hakurei-hsu-0.4.3> -``` - - - -## environment\.hakurei\.sharefs\.package - - - -The sharefs package to use\. - - - -*Type:* -package - - - -*Default:* - -```nix -<derivation sharefs> -``` - - - -## environment\.hakurei\.sharefs\.group - - - -Name of the group to run the sharefs daemon as\. - - - -*Type:* -string - - - -*Default:* - -```nix -"sharefs" -``` - - - -## environment\.hakurei\.sharefs\.name - - - -Host path to mount sharefs on\. - - - -*Type:* -string - - - -*Default:* - -```nix -"/sdcard" -``` - - - -## environment\.hakurei\.sharefs\.source - - - -Writable backing directory\. Setting this to null disables sharefs\. - - - -*Type:* -null or string - - - -*Default:* - -```nix -null -``` - - - -## environment\.hakurei\.sharefs\.user - - - -Name of the user to run the sharefs daemon as\. - - - -*Type:* -string - - - -*Default:* - -```nix -"sharefs" -``` - - - -## environment\.hakurei\.shell - - - -Absolute path to preferred shell\. - - - -*Type:* -string - - - -*Default:* - -```nix -"/run/current-system/sw/bin/bash" -``` - - - -## environment\.hakurei\.stateDir - - - -The state directory where app home directories are stored\. - - - -*Type:* -string - - - -## environment\.hakurei\.users - - - -Users allowed to spawn hakurei apps and their corresponding hakurei identity\. - - - -*Type:* -attribute set of integer between 0 and 99 (both inclusive) - - - -*Default:* - -```nix -{ } -``` - - diff --git a/test/options.nix b/test/options.nix deleted file mode 100644 index f624b6f5..00000000 --- a/test/options.nix +++ /dev/null @@ -1,364 +0,0 @@ -packages: -{ - lib, - pkgs, - config, - ... -}: - -let - inherit (lib) types mkOption mkEnableOption; - - cfg = config.environment.hakurei; -in - -{ - options = { - environment.hakurei = { - enable = mkEnableOption "hakurei"; - - package = mkOption { - type = types.package; - default = packages.${pkgs.stdenv.hostPlatform.system}.hakurei; - description = "The hakurei package to use."; - }; - - hsuPackage = mkOption { - type = types.package; - default = packages.${pkgs.stdenv.hostPlatform.system}.hsu; - description = "The hsu package to use."; - }; - - users = mkOption { - type = - let - inherit (types) attrsOf ints; - in - attrsOf (ints.between 0 99); - description = '' - Users allowed to spawn hakurei apps and their corresponding hakurei identity. - ''; - }; - - extraHomeConfig = mkOption { - type = types.anything; - description = '' - Extra home-manager configuration to merge with all target users. - ''; - }; - - sharefs = { - package = mkOption { - type = types.package; - default = pkgs.linkFarm "sharefs" { - "bin/sharefs" = "${cfg.package}/libexec/sharefs"; - "bin/mount.fuse.sharefs" = "${cfg.package}/libexec/sharefs"; - }; - description = "The sharefs package to use."; - }; - - user = mkOption { - type = types.str; - default = "sharefs"; - description = '' - Name of the user to run the sharefs daemon as. - ''; - }; - - group = mkOption { - type = types.str; - default = "sharefs"; - description = '' - Name of the group to run the sharefs daemon as. - ''; - }; - - name = mkOption { - type = types.str; - default = "/sdcard"; - description = '' - Host path to mount sharefs on. - ''; - }; - - source = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - Writable backing directory. Setting this to null disables sharefs. - ''; - }; - }; - - apps = mkOption { - type = - let - inherit (types) - int - ints - str - bool - enum - package - anything - submodule - listOf - attrsOf - nullOr - functionTo - ; - in - attrsOf (submodule { - options = { - name = mkOption { - type = str; - description = '' - Name of the app's launcher script. - ''; - }; - - verbose = mkEnableOption "launchers with verbose output"; - - identity = mkOption { - type = ints.between 1 9999; - description = '' - Application identity. Identity 0 is reserved for system services. - ''; - }; - shareUid = mkEnableOption "sharing identity with another application"; - - packages = mkOption { - type = listOf package; - default = [ ]; - description = '' - List of extra packages to install via home-manager. - ''; - }; - - extraConfig = mkOption { - type = anything; - default = { }; - description = '' - Extra home-manager configuration. - ''; - }; - - path = mkOption { - type = nullOr str; - default = null; - description = '' - Custom executable path. - Setting this to null will default to the start script. - ''; - }; - - args = mkOption { - type = nullOr (listOf str); - default = null; - description = '' - Custom args. - Setting this to null will default to script name. - ''; - }; - - script = mkOption { - type = nullOr str; - default = null; - description = '' - Application launch script. - ''; - }; - - command = mkOption { - type = nullOr str; - default = null; - description = '' - Command to run as the target user. - Setting this to null will default command to launcher name. - Has no effect when script is set. - ''; - }; - - groups = mkOption { - type = listOf str; - default = [ ]; - description = '' - List of groups to inherit from the privileged user. - ''; - }; - - shareRuntime = mkEnableOption "sharing of XDG_RUNTIME_DIR between containers under the same identity"; - shareTmpdir = mkEnableOption "sharing of TMPDIR between containers under the same identity"; - - dbus = { - session = mkOption { - type = nullOr (functionTo anything); - default = null; - description = '' - D-Bus session bus custom configuration. - Setting this to null will enable built-in defaults. - ''; - }; - - system = mkOption { - type = nullOr anything; - default = null; - description = '' - D-Bus system bus custom configuration. - Setting this to null will disable the system bus proxy. - ''; - }; - }; - - env = mkOption { - type = nullOr (attrsOf str); - default = null; - description = '' - Environment variables to set for the initial process in the sandbox. - ''; - }; - - wait_delay = mkOption { - type = nullOr int; - default = null; - description = '' - Duration to wait for after interrupting a container's initial process in nanoseconds. - A negative value causes the container to be terminated immediately on cancellation. - Setting this to null defaults to five seconds. - ''; - }; - - devel = mkEnableOption "debugging-related kernel interfaces"; - userns = mkEnableOption "user namespace creation"; - tty = mkEnableOption "access to the controlling terminal"; - multiarch = mkEnableOption "multiarch kernel-level support"; - - hostNet = mkEnableOption "share host net namespace" // { - default = true; - }; - hostAbstract = mkEnableOption "share abstract unix socket scope"; - - schedPolicy = mkOption { - type = nullOr (enum [ - "fifo" - "rr" - "batch" - "idle" - "deadline" - "ext" - ]); - default = null; - description = '' - Scheduling policy to set for the container. - The zero value retains the current scheduling policy. - ''; - }; - schedPriority = mkOption { - type = nullOr (ints.between 1 99); - default = null; - description = '' - Scheduling priority to set for the container. - ''; - }; - - nix = mkEnableOption "nix daemon access"; - mapRealUid = mkEnableOption "mapping to priv-user uid"; - device = mkEnableOption "access to all devices"; - insecureWayland = mkEnableOption "direct access to the Wayland socket"; - - gpu = mkOption { - type = nullOr bool; - default = null; - description = '' - Target process GPU and driver access. - Setting this to null will enable GPU whenever X or Wayland is enabled. - ''; - }; - - useCommonPaths = mkEnableOption "common extra paths" // { - default = true; - }; - - extraPaths = mkOption { - type = listOf (attrsOf anything); - default = [ ]; - description = '' - Extra paths to make available to the container. - ''; - }; - - enablements = { - wayland = mkOption { - type = nullOr bool; - default = true; - description = '' - Whether to share the Wayland server via security-context-v1. - ''; - }; - - x11 = mkOption { - type = nullOr bool; - default = false; - description = '' - Whether to share the X11 socket and allow connection. - ''; - }; - - dbus = mkOption { - type = nullOr bool; - default = true; - description = '' - Whether to proxy D-Bus. - ''; - }; - - pipewire = mkOption { - type = nullOr bool; - default = true; - description = '' - Whether to share the PipeWire server via pipewire-pulse on a SecurityContext socket. - ''; - }; - }; - - share = mkOption { - type = nullOr package; - default = null; - description = '' - Package containing share files. - Setting this to null will default package name to wrapper name. - ''; - }; - }; - }); - default = { }; - description = '' - Declaratively configured hakurei apps. - ''; - }; - - commonPaths = mkOption { - type = types.listOf (types.attrsOf types.anything); - default = [ ]; - description = '' - Common extra paths to make available to the container. - ''; - }; - - shell = mkOption { - type = types.str; - default = "/run/current-system/sw/bin/bash"; - description = '' - Absolute path to preferred shell. - ''; - }; - - stateDir = mkOption { - type = types.str; - description = '' - The state directory where app home directories are stored. - ''; - }; - }; - }; -} diff --git a/test/package.nix b/test/package.nix deleted file mode 100644 index e68420e3..00000000 --- a/test/package.nix +++ /dev/null @@ -1,144 +0,0 @@ -{ - lib, - stdenv, - buildGo127Module, - makeBinaryWrapper, - xdg-dbus-proxy, - pkg-config, - libffi, - libseccomp, - acl, - wayland, - wayland-protocols, - wayland-scanner, - - libxcb, - libxau, - libxdmcp, - - # for sharefs - fuse3, - - # for passthru.buildInputs - go_1_27, - clang, - xorgproto, - - # for check - util-linux, - nettools, - - glibc, # for ldd - withStatic ? stdenv.hostPlatform.isStatic, -}: - -buildGo127Module rec { - pname = "hakurei"; - version = with lib.strings; removePrefix "v" (trim (builtins.readFile ../cmd/dist/VERSION)); - - srcFiltered = builtins.path { - name = "${pname}-src"; - path = lib.cleanSource ../.; - filter = path: type: !(type == "regular" && (lib.hasSuffix ".nix" path || lib.hasSuffix ".py" path)) && !(type == "directory" && lib.hasSuffix "/test" path) && !(type == "directory" && lib.hasSuffix "/cmd/hsu" path); - }; - vendorHash = null; - - src = stdenv.mkDerivation { - name = "${pname}-src-full"; - inherit version; - enableParallelBuilding = true; - src = srcFiltered; - - buildInputs = [ - wayland - wayland-protocols - ]; - - nativeBuildInputs = [ - go_1_27 - pkg-config - wayland-scanner - ]; - - buildPhase = "GOCACHE=$(mktemp -d) go generate ./..."; - installPhase = "cp -r . $out"; - }; - - ldflags = - lib.attrsets.foldlAttrs - ( - ldflags: name: value: - ldflags ++ [ "-X hakurei.app/internal/info.${name}=${value}" ] - ) - ( - [ "-s -w" ] - ++ lib.optionals withStatic [ - "-linkmode external" - "-extldflags \"-static\"" - ] - ) - { - buildVersion = "v${version}"; - hakureiPath = "${placeholder "out"}/libexec/hakurei"; - hsuPath = "/run/wrappers/bin/hsu"; - }; - - env = { - # use clang instead of gcc - CC = "clang -O3 -Werror"; - }; - - buildInputs = [ - libffi - libseccomp - fuse3 - acl - wayland - - libxcb - libxau - libxdmcp - ]; - - nativeBuildInputs = [ - pkg-config - makeBinaryWrapper - - # for container example - nettools - ]; - - postInstall = - let - appPackages = [ - glibc - xdg-dbus-proxy - ]; - in - '' - install -D --target-directory=$out/share/zsh/site-functions cmd/dist/comp/* - - mkdir "$out/libexec" - mv "$out"/bin/* "$out/libexec/" - - makeBinaryWrapper "$out/libexec/hakurei" "$out/bin/hakurei" \ - --inherit-argv0 --prefix PATH : ${lib.makeBinPath appPackages} - ''; - - passthru = { - go = go_1_27; - - targetPkgs = [ - go_1_27 - clang - xorgproto - util-linux - - # for go generate - wayland-protocols - wayland-scanner - ] - ++ buildInputs - ++ nativeBuildInputs; - }; -} diff --git a/test/sandbox/case/default.nix b/test/sandbox/case/default.nix deleted file mode 100644 index 337f4bf2..00000000 --- a/test/sandbox/case/default.nix +++ /dev/null @@ -1,97 +0,0 @@ -system: lib: testProgram: -let - fs = mode: dir: data: { - mode = lib.fromHexString mode; - inherit - dir - data - ; - }; - - ignore = "//ignore"; - - ent = root: target: vfs_optstr: fstype: source: fs_optstr: { - id = -1; - parent = -1; - inherit - root - target - vfs_optstr - fstype - source - fs_optstr - ; - }; - - importTestCase = - path: - import path { - inherit - fs - ent - ignore - system - ; - }; - - callTestCase = - path: identity: - let - tc = importTestCase path; - in - { - name = "check-sandbox-${tc.name}"; - inherit identity; - verbose = true; - inherit (tc) - tty - device - mapRealUid - useCommonPaths - userns - hostAbstract - shareRuntime - shareTmpdir - ; - enablements = { - inherit (tc) x11; - }; - share = testProgram; - packages = [ ]; - path = "${testProgram}/bin/hakurei-test"; - args = [ - "hakurei-test" - "-p" - "/var/tmp/.hakurei-check-ok.${toString identity}" - "-t" - (toString (builtins.toFile "hakurei-${tc.name}-want.json" (builtins.toJSON tc.want))) - "-s" - tc.expectedFilter.${system} - ]; - - extraPaths = - if tc.useCommonPaths then - [ ] - else - [ - { - type = "bind"; - src = "/var/tmp"; - write = true; - } - ]; - }; - - testCaseName = name: "cat.gensokyo.hakurei.test." + name; -in -{ - apps = { - ${testCaseName "preset"} = callTestCase ./preset.nix 1; - ${testCaseName "tty"} = callTestCase ./tty.nix 2; - ${testCaseName "mapuid"} = callTestCase ./mapuid.nix 3; - ${testCaseName "device"} = callTestCase ./device.nix 4; - ${testCaseName "pdlike"} = callTestCase ./pdlike.nix 5; - }; - - pd = importTestCase ./pd.nix; -} diff --git a/test/sandbox/case/device.nix b/test/sandbox/case/device.nix deleted file mode 100644 index 889e0a06..00000000 --- a/test/sandbox/case/device.nix +++ /dev/null @@ -1,255 +0,0 @@ -{ - fs, - ent, - ignore, - system, -}: -let - extraPaths = { - x86_64-linux = { - fd = "fd0"; - sr = { - sr0 = fs "80001ff" null null; - }; - }; - aarch64-linux = { - fd = "mtdblock0"; - sr = { }; - }; - }; -in -{ - name = "device"; - tty = false; - device = true; - mapRealUid = false; - useCommonPaths = true; - userns = false; - x11 = true; - hostAbstract = false; - shareRuntime = false; - shareTmpdir = true; - - # 0, PresetStrict - expectedFilter = { - x86_64-linux = "e880298df2bd6751d0040fc21bc0ed4c00f95dc0d7ba506c244d8b8cf6866dba8ef4a33296f287b66cccc1d78e97026597f84cc7dec1573e148960fbd35cd735"; - aarch64-linux = "79318538a3dc851314b6bd96f10d5861acb2aa7e13cb8de0619d0f6a76709d67f01ef3fd67e195862b02f9711e5b769bc4d1eb4fc0dfc41a723c89c968a93297"; - }; - - want = { - env = [ - "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus" - "DISPLAY=unix:/tmp/.X11-unix/X0" - "HOME=/var/lib/hakurei/u0/a4" - "SHELL=/run/current-system/sw/bin/bash" - "TERM=linux" - "USER=u0_a4" - "WAYLAND_DISPLAY=wayland-0" - "XDG_RUNTIME_DIR=/run/user/65534" - "XDG_SESSION_CLASS=user" - "XDG_SESSION_TYPE=wayland" - "PULSE_SERVER=unix:/run/user/65534/pulse/native" - ]; - - fs = fs "dead" { - ".hakurei" = fs "800001ed" { - ".ro-store" = fs "801001fd" null null; - store = fs "800001ff" null null; - } null; - bin = fs "800001ed" { sh = fs "80001ff" null null; } null; - dev = fs "800001ed" null null; - etc = fs "800001ed" { - ".clean" = fs "80001ff" null null; - ".host" = fs "800001c0" null null; - ".updated" = fs "80001ff" null null; - "NIXOS" = fs "80001ff" null null; - "X11" = fs "80001ff" null null; - "alsa" = fs "80001ff" null null; - "bash_logout" = fs "80001ff" null null; - "bashrc" = fs "80001ff" null null; - "binfmt.d" = fs "80001ff" null null; - "dbus-1" = fs "80001ff" null null; - "default" = fs "80001ff" null null; - "dhcpcd.exit-hook" = fs "80001ff" null null; - "environment.d" = fs "80001ff" null null; - "fonts" = fs "80001ff" null null; - "fstab" = fs "80001ff" null null; - "hsurc" = fs "80001ff" null null; - "fuse.conf" = fs "80001ff" null null; - "gai.conf" = fs "80001ff" null null; - "group" = fs "180" null "hakurei:x:65534:\n"; - "host.conf" = fs "80001ff" null null; - "hostname" = fs "80001ff" null null; - "hosts" = fs "80001ff" null null; - "inputrc" = fs "80001ff" null null; - "issue" = fs "80001ff" null null; - "kbd" = fs "80001ff" null null; - "locale.conf" = fs "80001ff" null null; - "login.defs" = fs "80001ff" null null; - "lsb-release" = fs "80001ff" null null; - "lvm" = fs "80001ff" null null; - "machine-id" = fs "80001ff" null null; - "man_db.conf" = fs "80001ff" null null; - "modprobe.d" = fs "80001ff" null null; - "modules-load.d" = fs "80001ff" null null; - "mtab" = fs "80001ff" null null; - "nanorc" = fs "80001ff" null null; - "netgroup" = fs "80001ff" null null; - "nix" = fs "80001ff" null null; - "nixos" = fs "80001ff" null null; - "nscd.conf" = fs "80001ff" null null; - "nsswitch.conf" = fs "80001ff" null null; - "os-release" = fs "80001ff" null null; - "pam" = fs "80001ff" null null; - "pam.d" = fs "80001ff" null null; - "passwd" = fs "180" null "u0_a4:x:65534:65534:Hakurei:/var/lib/hakurei/u0/a4:/run/current-system/sw/bin/bash\n"; - "pipewire" = fs "80001ff" null null; - "pki" = fs "80001ff" null null; - "polkit-1" = fs "80001ff" null null; - "profile" = fs "80001ff" null null; - "protocols" = fs "80001ff" null null; - "resolv.conf" = fs "80001ff" null null; - "resolvconf.conf" = fs "80001ff" null null; - "rpc" = fs "80001ff" null null; - "services" = fs "80001ff" null null; - "set-environment" = fs "80001ff" null null; - "shadow" = fs "80001ff" null null; - "shells" = fs "80001ff" null null; - "speech-dispatcher" = fs "80001ff" null null; - "ssh" = fs "80001ff" null null; - "ssl" = fs "80001ff" null null; - "static" = fs "80001ff" null null; - "subgid" = fs "80001ff" null null; - "subuid" = fs "80001ff" null null; - "sudoers" = fs "80001ff" null null; - "sway" = fs "80001ff" null null; - "sysctl.d" = fs "80001ff" null null; - "systemd" = fs "80001ff" null null; - "terminfo" = fs "80001ff" null null; - "tmpfiles.d" = fs "80001ff" null null; - "udev" = fs "80001ff" null null; - "vconsole.conf" = fs "80001ff" null null; - "xdg" = fs "80001ff" null null; - "zoneinfo" = fs "80001ff" null null; - } null; - nix = fs "800001c0" { store = fs "801001fd" null null; } null; - proc = fs "8000016d" null null; - run = fs "800001ed" { - current-system = fs "80001ff" null null; - opengl-driver = fs "80001ff" null null; - user = fs "800001ed" { - "65534" = fs "800001c0" { - bus = fs "10001fd" null null; - pulse = fs "800001c0" { native = fs "10001ff" null null; } null; - wayland-0 = fs "1000038" null null; - } null; - } null; - } null; - sys = fs "800001c0" { - block = fs "800001ed" ( - { - ${extraPaths.${system}.fd} = fs "80001ff" null null; - loop0 = fs "80001ff" null null; - loop1 = fs "80001ff" null null; - loop2 = fs "80001ff" null null; - loop3 = fs "80001ff" null null; - loop4 = fs "80001ff" null null; - loop5 = fs "80001ff" null null; - loop6 = fs "80001ff" null null; - loop7 = fs "80001ff" null null; - vda = fs "80001ff" null null; - } - // extraPaths.${system}.sr - ) null; - bus = fs "800001ed" null null; - class = fs "800001ed" null null; - dev = fs "800001ed" { - block = fs "800001ed" null null; - char = fs "800001ed" null null; - } null; - devices = fs "800001ed" null null; - } null; - tmp = fs "800001f8" { - ".X11-unix" = fs "801001ff" { X0 = fs "10001fd" null null; } null; - } null; - usr = fs "800001c0" { bin = fs "800001ed" { env = fs "80001ff" null null; } null; } null; - var = fs "800001c0" { - tmp = fs "801001ff" null null; - lib = fs "800001c0" { - hakurei = fs "800001c0" { - u0 = fs "800001c0" { - a4 = fs "800001c0" { - ".cache" = fs "800001ed" { ".keep" = fs "80001ff" null ""; } null; - ".config" = fs "800001ed" { - "environment.d" = fs "800001ed" { "10-home-manager.conf" = fs "80001ff" null null; } null; - systemd = fs "800001ed" { - user = fs "800001ed" { "tray.target" = fs "80001ff" null null; } null; - } null; - } null; - ".local" = fs "800001ed" { - state = fs "800001ed" { - ".keep" = fs "80001ff" null ""; - home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null; - nix = fs "800001ed" { - profiles = fs "800001ed" { - profile = fs "80001ff" null null; - profile-1-link = fs "80001ff" null null; - } null; - } null; - } null; - } null; - ".nix-defexpr" = fs "800001ed" { - channels = fs "80001ff" null null; - channels_root = fs "80001ff" null null; - } null; - ".nix-profile" = fs "80001ff" null null; - } null; - } null; - } null; - } null; - cache = fs "800001ed" { private = fs "800001c0" null null; } null; - } null; - } null; - - mount = [ - (ent "/sysroot" "/" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10004,gid=10004") - (ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw") - (ent "/" "/.hakurei" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=10004,gid=10004") - (ent "/" "/dev" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/" "/dev/pts" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666") - (ent "/" ignore ignore ignore ignore ignore) # not deterministic - (ent "/" ignore ignore ignore ignore ignore) - (ent "/" ignore ignore ignore ignore ignore) - (ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10004,gid=10004") - (ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10004,gid=10004") - (ent "/tmp/hakurei.0/tmpdir/4" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10004,gid=10004") - (ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10004,gid=10004") - (ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/tmp/.X11-unix" "/tmp/.X11-unix" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/sysroot/nix/.ro-store,upperdir=/sysroot/nix/.rw-store/upper,workdir=/sysroot/nix/.rw-store/work,uuid=on") - (ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/class" "/sys/class" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir+=/host/nix/.ro-store,lowerdir+=/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr") - (ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir+=/host/nix/.ro-store,lowerdir+=/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,userxattr") - (ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/var/lib/hakurei/u0/a4" "/var/lib/hakurei/u0/a4" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - ]; - - seccomp = true; - - try_socket = "/tmp/.X11-unix/X0"; - socket_abstract = false; - socket_pathname = true; - }; -} diff --git a/test/sandbox/case/mapuid.nix b/test/sandbox/case/mapuid.nix deleted file mode 100644 index 1b6ef0e7..00000000 --- a/test/sandbox/case/mapuid.nix +++ /dev/null @@ -1,282 +0,0 @@ -{ - fs, - ent, - ignore, - system, -}: -let - extraPaths = { - x86_64-linux = { - fd = "fd0"; - "/dev/dri" = { - by-path = fs "800001ed" { - "pci-0000:00:09.0-card" = fs "80001ff" null null; - "pci-0000:00:09.0-render" = fs "80001ff" null null; - } null; - card0 = fs "42001b0" null null; - renderD128 = fs "42001b6" null null; - }; - sr = { - sr0 = fs "80001ff" null null; - }; - }; - aarch64-linux = { - fd = "mtdblock0"; - "/dev/dri" = null; - sr = { }; - }; - }; -in -{ - name = "mapuid"; - tty = false; - device = false; - mapRealUid = true; - useCommonPaths = true; - userns = false; - x11 = false; - hostAbstract = false; - shareRuntime = true; - shareTmpdir = true; - - # 0, PresetStrict - expectedFilter = { - x86_64-linux = "e880298df2bd6751d0040fc21bc0ed4c00f95dc0d7ba506c244d8b8cf6866dba8ef4a33296f287b66cccc1d78e97026597f84cc7dec1573e148960fbd35cd735"; - aarch64-linux = "79318538a3dc851314b6bd96f10d5861acb2aa7e13cb8de0619d0f6a76709d67f01ef3fd67e195862b02f9711e5b769bc4d1eb4fc0dfc41a723c89c968a93297"; - }; - - want = { - env = [ - "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus" - "HOME=/var/lib/hakurei/u0/a3" - "SHELL=/run/current-system/sw/bin/bash" - "TERM=linux" - "USER=u0_a3" - "WAYLAND_DISPLAY=wayland-0" - "XDG_RUNTIME_DIR=/run/user/1000" - "XDG_SESSION_CLASS=user" - "XDG_SESSION_TYPE=wayland" - "PULSE_SERVER=unix:/run/user/1000/pulse/native" - ]; - - fs = fs "dead" { - ".hakurei" = fs "800001ed" { - ".ro-store" = fs "801001fd" null null; - store = fs "800001ff" null null; - } null; - bin = fs "800001ed" { sh = fs "80001ff" null null; } null; - dev = fs "800001ed" { - core = fs "80001ff" null null; - dri = fs "800001ed" extraPaths.${system}."/dev/dri" null; - fd = fs "80001ff" null null; - full = fs "42001b6" null null; - mqueue = fs "801001ff" { } null; - null = fs "42001b6" null ""; - ptmx = fs "80001ff" null null; - pts = fs "800001ed" { ptmx = fs "42001b6" null null; } null; - random = fs "42001b6" null null; - shm = fs "801001ff" { } null; - stderr = fs "80001ff" null null; - stdin = fs "80001ff" null null; - stdout = fs "80001ff" null null; - tty = fs "42001b6" null null; - urandom = fs "42001b6" null null; - zero = fs "42001b6" null null; - } null; - etc = fs "800001ed" { - ".clean" = fs "80001ff" null null; - ".host" = fs "800001c0" null null; - ".updated" = fs "80001ff" null null; - "NIXOS" = fs "80001ff" null null; - "X11" = fs "80001ff" null null; - "alsa" = fs "80001ff" null null; - "bash_logout" = fs "80001ff" null null; - "bashrc" = fs "80001ff" null null; - "binfmt.d" = fs "80001ff" null null; - "dbus-1" = fs "80001ff" null null; - "default" = fs "80001ff" null null; - "dhcpcd.exit-hook" = fs "80001ff" null null; - "environment.d" = fs "80001ff" null null; - "fonts" = fs "80001ff" null null; - "fstab" = fs "80001ff" null null; - "hsurc" = fs "80001ff" null null; - "fuse.conf" = fs "80001ff" null null; - "gai.conf" = fs "80001ff" null null; - "group" = fs "180" null "hakurei:x:100:\n"; - "host.conf" = fs "80001ff" null null; - "hostname" = fs "80001ff" null null; - "hosts" = fs "80001ff" null null; - "inputrc" = fs "80001ff" null null; - "issue" = fs "80001ff" null null; - "kbd" = fs "80001ff" null null; - "locale.conf" = fs "80001ff" null null; - "login.defs" = fs "80001ff" null null; - "lsb-release" = fs "80001ff" null null; - "lvm" = fs "80001ff" null null; - "machine-id" = fs "80001ff" null null; - "man_db.conf" = fs "80001ff" null null; - "modprobe.d" = fs "80001ff" null null; - "modules-load.d" = fs "80001ff" null null; - "mtab" = fs "80001ff" null null; - "nanorc" = fs "80001ff" null null; - "netgroup" = fs "80001ff" null null; - "nix" = fs "80001ff" null null; - "nixos" = fs "80001ff" null null; - "nscd.conf" = fs "80001ff" null null; - "nsswitch.conf" = fs "80001ff" null null; - "os-release" = fs "80001ff" null null; - "pam" = fs "80001ff" null null; - "pam.d" = fs "80001ff" null null; - "passwd" = fs "180" null "u0_a3:x:1000:100:Hakurei:/var/lib/hakurei/u0/a3:/run/current-system/sw/bin/bash\n"; - "pipewire" = fs "80001ff" null null; - "pki" = fs "80001ff" null null; - "polkit-1" = fs "80001ff" null null; - "profile" = fs "80001ff" null null; - "protocols" = fs "80001ff" null null; - "resolv.conf" = fs "80001ff" null null; - "resolvconf.conf" = fs "80001ff" null null; - "rpc" = fs "80001ff" null null; - "services" = fs "80001ff" null null; - "set-environment" = fs "80001ff" null null; - "shadow" = fs "80001ff" null null; - "shells" = fs "80001ff" null null; - "speech-dispatcher" = fs "80001ff" null null; - "ssh" = fs "80001ff" null null; - "ssl" = fs "80001ff" null null; - "static" = fs "80001ff" null null; - "subgid" = fs "80001ff" null null; - "subuid" = fs "80001ff" null null; - "sudoers" = fs "80001ff" null null; - "sway" = fs "80001ff" null null; - "sysctl.d" = fs "80001ff" null null; - "systemd" = fs "80001ff" null null; - "terminfo" = fs "80001ff" null null; - "tmpfiles.d" = fs "80001ff" null null; - "udev" = fs "80001ff" null null; - "vconsole.conf" = fs "80001ff" null null; - "xdg" = fs "80001ff" null null; - "zoneinfo" = fs "80001ff" null null; - } null; - nix = fs "800001c0" { store = fs "801001fd" null null; } null; - proc = fs "8000016d" null null; - run = fs "800001ed" { - current-system = fs "80001ff" null null; - opengl-driver = fs "80001ff" null null; - user = fs "800001ed" { - "1000" = fs "800001f8" { - bus = fs "10001fd" null null; - pulse = fs "800001c0" { native = fs "10001ff" null null; } null; - wayland-0 = fs "1000038" null null; - } null; - } null; - } null; - sys = fs "800001c0" { - block = fs "800001ed" ( - { - ${extraPaths.${system}.fd} = fs "80001ff" null null; - loop0 = fs "80001ff" null null; - loop1 = fs "80001ff" null null; - loop2 = fs "80001ff" null null; - loop3 = fs "80001ff" null null; - loop4 = fs "80001ff" null null; - loop5 = fs "80001ff" null null; - loop6 = fs "80001ff" null null; - loop7 = fs "80001ff" null null; - vda = fs "80001ff" null null; - } - // extraPaths.${system}.sr - ) null; - bus = fs "800001ed" null null; - class = fs "800001ed" null null; - dev = fs "800001ed" { - block = fs "800001ed" null null; - char = fs "800001ed" null null; - } null; - devices = fs "800001ed" null null; - } null; - tmp = fs "800001f8" { } null; - usr = fs "800001c0" { bin = fs "800001ed" { env = fs "80001ff" null null; } null; } null; - var = fs "800001c0" { - tmp = fs "801001ff" null null; - lib = fs "800001c0" { - hakurei = fs "800001c0" { - u0 = fs "800001c0" { - a3 = fs "800001c0" { - ".cache" = fs "800001ed" { ".keep" = fs "80001ff" null ""; } null; - ".config" = fs "800001ed" { - "environment.d" = fs "800001ed" { "10-home-manager.conf" = fs "80001ff" null null; } null; - systemd = fs "800001ed" { - user = fs "800001ed" { "tray.target" = fs "80001ff" null null; } null; - } null; - } null; - ".local" = fs "800001ed" { - state = fs "800001ed" { - ".keep" = fs "80001ff" null ""; - home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null; - nix = fs "800001ed" { - profiles = fs "800001ed" { - profile = fs "80001ff" null null; - profile-1-link = fs "80001ff" null null; - } null; - } null; - } null; - } null; - ".nix-defexpr" = fs "800001ed" { - channels = fs "80001ff" null null; - channels_root = fs "80001ff" null null; - } null; - ".nix-profile" = fs "80001ff" null null; - } null; - } null; - } null; - } null; - cache = fs "800001ed" { private = fs "800001c0" null null; } null; - } null; - } null; - - mount = [ - (ent "/sysroot" "/" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10003,gid=10003") - (ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw") - (ent "/" "/.hakurei" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=10003,gid=10003") - (ent "/" "/dev" "ro,nosuid,nodev,relatime" "tmpfs" "devtmpfs" "rw,mode=755,uid=10003,gid=10003") - (ent "/null" "/dev/null" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/zero" "/dev/zero" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/full" "/dev/full" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/random" "/dev/random" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/urandom" "/dev/urandom" "rw,nosuid" "devtmpfs" "devtmpfs" 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/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw") - (ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10003,gid=10003") - (ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10003,gid=10003") - (ent "/tmp/hakurei.0/runtime/3" "/run/user/1000" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/tmp/hakurei.0/tmpdir/3" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10003,gid=10003") - (ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10003,gid=10003") - (ent ignore "/run/user/1000/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent ignore "/run/user/1000/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/sysroot/nix/.ro-store,upperdir=/sysroot/nix/.rw-store/upper,workdir=/sysroot/nix/.rw-store/work,uuid=on") - (ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/class" "/sys/class" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir+=/host/nix/.ro-store,lowerdir+=/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr") - (ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir+=/host/nix/.ro-store,lowerdir+=/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,userxattr") - (ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/var/lib/hakurei/u0/a3" "/var/lib/hakurei/u0/a3" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent ignore "/run/user/1000/pulse/native" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - ]; - - seccomp = true; - - try_socket = "/tmp/.X11-unix/X0"; - socket_abstract = false; - socket_pathname = false; - }; -} diff --git a/test/sandbox/case/pd.nix b/test/sandbox/case/pd.nix deleted file mode 100644 index 25f42979..00000000 --- a/test/sandbox/case/pd.nix +++ /dev/null @@ -1,206 +0,0 @@ -{ - fs, - ent, - ignore, - ... -}: -{ - # 0, PresetExt | PresetDenyDevel - expectedFilter = { - x86_64-linux = "c698b081ff957afe17a6d94374537d37f2a63f6f9dd75da7546542407a9e32476ebda3312ba7785d7f618542bcfaf27ca27dcc2dddba852069d28bcfe8cad39a"; - aarch64-linux = "433ce9b911282d6dcc8029319fb79b816b60d5a795ec8fc94344dd027614d68f023166a91bb881faaeeedd26e3d89474e141e5a69a97e93b8984ca8f14999980"; - }; - - want = { - env = [ - "HOME=/var/lib/hakurei/u0/a0" - "SHELL=/run/current-system/sw/bin/bash" - "TERM=linux" - "USER=u0_a0" - "XDG_RUNTIME_DIR=/run/user/65534" - "XDG_SESSION_CLASS=user" - "XDG_SESSION_TYPE=tty" - ]; - - fs = fs "dead" { - ".hakurei" = fs "800001ed" { } null; - bin = fs "800001ed" { sh = fs "80001ff" null null; } null; - dev = fs "800001ed" { - console = fs "4200190" null null; - core = fs "80001ff" null null; - fd = fs "80001ff" null null; - full = fs "42001b6" null null; - kvm = fs "42001b6" null null; - mqueue = fs "801001ff" { } null; - null = fs "42001b6" null ""; - ptmx = fs "80001ff" null null; - pts = fs "800001ed" { ptmx = fs "42001b6" null null; } null; - random = fs "42001b6" null null; - shm = fs "801001ff" { } null; - stderr = fs "80001ff" null null; - stdin = fs "80001ff" null null; - stdout = fs "80001ff" null null; - tty = fs "42001b6" null null; - urandom = fs "42001b6" null null; - zero = fs "42001b6" null null; - } null; - etc = fs "800001ed" { - ".clean" = fs "80001ff" null null; - ".host" = fs "800001c0" null null; - ".updated" = fs "80001ff" null null; - "NIXOS" = fs "80001ff" null null; - "X11" = fs "80001ff" null null; - "alsa" = fs "80001ff" null null; - "bash_logout" = fs "80001ff" null null; - "bashrc" = fs "80001ff" null null; - "binfmt.d" = fs "80001ff" null null; - "dbus-1" = fs "80001ff" null null; - "default" = fs "80001ff" null null; - "dhcpcd.exit-hook" = fs "80001ff" null null; - "environment.d" = fs "80001ff" null null; - "fonts" = fs "80001ff" null null; - "fstab" = fs "80001ff" null null; - "hsurc" = fs "80001ff" null null; - "fuse.conf" = fs "80001ff" null null; - "gai.conf" = fs "80001ff" null null; - "group" = fs "180" null "hakurei:x:65534:\n"; - "host.conf" = fs "80001ff" null null; - "hostname" = fs "80001ff" null null; - "hosts" = fs "80001ff" null null; - "inputrc" = fs "80001ff" null null; - "issue" = fs "80001ff" null null; - "kbd" = fs "80001ff" null null; - "locale.conf" = fs "80001ff" null null; - "login.defs" = fs "80001ff" null null; - "lsb-release" = fs "80001ff" null null; - "lvm" = fs "80001ff" null null; - "machine-id" = fs "80001ff" null null; - "man_db.conf" = fs "80001ff" null null; - "modprobe.d" = fs "80001ff" null null; - "modules-load.d" = fs "80001ff" null null; - "mtab" = fs "80001ff" null null; - "nanorc" = fs "80001ff" null null; - "netgroup" = fs "80001ff" null null; - "nix" = fs "80001ff" null null; - "nixos" = fs "80001ff" null null; - "nscd.conf" = fs "80001ff" null null; - "nsswitch.conf" = fs "80001ff" null null; - "os-release" = fs "80001ff" null null; - "pam" = fs "80001ff" null null; - "pam.d" = fs "80001ff" null null; - "passwd" = fs "180" null "u0_a0:x:65534:65534:Hakurei:/var/lib/hakurei/u0/a0:/run/current-system/sw/bin/bash\n"; - "pipewire" = fs "80001ff" null null; - "pki" = fs "80001ff" null null; - "polkit-1" = fs "80001ff" null null; - "profile" = fs "80001ff" null null; - "protocols" = fs "80001ff" null null; - "resolv.conf" = fs "80001ff" null null; - "resolvconf.conf" = fs "80001ff" null null; - "rpc" = fs "80001ff" null null; - "services" = fs "80001ff" null null; - "set-environment" = fs "80001ff" null null; - "shadow" = fs "80001ff" null null; - "shells" = fs "80001ff" null null; - "speech-dispatcher" = fs "80001ff" null null; - "ssh" = fs "80001ff" null null; - "ssl" = fs "80001ff" null null; - "static" = fs "80001ff" null null; - "subgid" = fs "80001ff" null null; - "subuid" = fs "80001ff" null null; - "sudoers" = fs "80001ff" null null; - "sway" = fs "80001ff" null null; - "sysctl.d" = fs "80001ff" null null; - "systemd" = fs "80001ff" null null; - "terminfo" = fs "80001ff" null null; - "tmpfiles.d" = fs "80001ff" null null; - "udev" = fs "80001ff" null null; - "vconsole.conf" = fs "80001ff" null null; - "xdg" = fs "80001ff" null null; - "zoneinfo" = fs "80001ff" null null; - } null; - home = fs "800001ed" { alice = fs "800001c0" null null; } null; - lib64 = fs "800001ed" { "ld-linux-x86-64.so.2" = fs "80001ff" null null; } null; - "lost+found" = fs "800001c0" null null; - nix = fs "800001ed" { - ".ro-store" = fs "801001fd" null null; - ".rw-store" = fs "800001ed" null null; - store = fs "801001fd" null null; - var = fs "800001ed" { - log = fs "800001ed" null null; - nix = fs "800001ed" null null; - } null; - } null; - proc = fs "8000016d" null null; - root = fs "800001c0" null null; - run = fs "800001ed" null null; - srv = fs "800001ed" { } null; - sys = fs "8000016d" null null; - tmp = fs "800001f8" { } null; - usr = fs "800001ed" { bin = fs "800001ed" { env = fs "80001ff" null null; } null; } null; - var = fs "800001ed" null null; - } null; - - mount = [ - (ent "/sysroot" "/" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10000,gid=10000") - (ent "/bin" "/bin" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/home" "/home" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/lib64" "/lib64" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/lost+found" "/lost+found" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/nix" "/nix" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - # systemd nondeterminism: ro-store rw-store - (ent "/" ignore "rw,nosuid,nodev,relatime" ignore ignore ignore) - (ent "/" ignore "rw,nosuid,nodev,relatime" ignore ignore ignore) - (ent "/" "/nix/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir=/sysroot/nix/.ro-store,upperdir=/sysroot/nix/.rw-store/upper,workdir=/sysroot/nix/.rw-store/work,uuid=on") - (ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/sysroot/nix/.ro-store,upperdir=/sysroot/nix/.rw-store/upper,workdir=/sysroot/nix/.rw-store/work,uuid=on") - (ent "/root" "/root" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/" "/run" "rw,nosuid,nodev" "tmpfs" "tmpfs" ignore) - (ent "/" "/run/keys" "rw,nosuid,nodev,relatime" "ramfs" "ramfs" "rw,mode=750") - (ent "/" "/run/credentials/systemd-journald.service" "rw,nosuid,nodev,noexec,relatime,nosymfollow" "tmpfs" "none" "ro,size=1024k,nr_inodes=1024,mode=700,noswap") - (ent "/" "/run/wrappers" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore) - (ent "/" "/run/credentials/getty@tty1.service" "rw,nosuid,nodev,noexec,relatime,nosymfollow" "tmpfs" "none" "ro,size=1024k,nr_inodes=1024,mode=700,noswap") - (ent "/" "/run/user/1000" "rw,nosuid,nodev,relatime" "tmpfs" "tmpfs" ignore) - (ent "/srv" "/srv" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/" "/sys" "rw,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/" "/sys/kernel/security" "rw,nosuid,nodev,noexec,relatime" "securityfs" "securityfs" "rw") - (ent "/../../.." "/sys/fs/cgroup" "rw,nosuid,nodev,noexec,relatime" "cgroup2" "cgroup2" "rw,nsdelegate,memory_recursiveprot,memory_hugetlb_accounting") - (ent "/" "/sys/fs/pstore" "rw,nosuid,nodev,noexec,relatime" "pstore" "none" "rw") - (ent "/" "/sys/fs/bpf" "rw,nosuid,nodev,noexec,relatime" "bpf" "bpf" "rw,mode=700") - # systemd nondeterminism: tracefs debugfs configfs fusectl - (ent "/" ignore "rw,nosuid,nodev,noexec,relatime" ignore ignore "rw") - (ent "/" ignore "rw,nosuid,nodev,noexec,relatime" ignore ignore "rw") - (ent "/" ignore "rw,nosuid,nodev,noexec,relatime" ignore ignore "rw") - (ent "/" ignore "rw,nosuid,nodev,noexec,relatime" ignore ignore "rw") - (ent "/usr" "/usr" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/var" "/var" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw") - (ent "/" "/.hakurei" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=10000,gid=10000") - (ent "/" "/dev" "ro,nosuid,nodev,relatime" "tmpfs" "devtmpfs" "rw,mode=755,uid=10000,gid=10000") - (ent "/null" "/dev/null" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/zero" "/dev/zero" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/full" "/dev/full" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/random" "/dev/random" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/urandom" "/dev/urandom" "rw,nosuid" "devtmpfs" "devtmpfs" 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 ignore "/dev/console" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666") - (ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw") - (ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10000,gid=10000") - (ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10000,gid=10000") - (ent "/tmp/hakurei.0/runtime/0" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/tmp/hakurei.0/tmpdir/0" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10000,gid=10000") - (ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10000,gid=10000") - (ent "/kvm" "/dev/kvm" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/" "/run/user/1000" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=8k,mode=755,uid=10000,gid=10000") - (ent "/" "/run/nscd" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=8k,mode=755,uid=10000,gid=10000") - (ent "/" "/run/dbus" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=8k,mode=755,uid=10000,gid=10000") - ]; - - seccomp = true; - - try_socket = "/tmp/.X11-unix/X0"; - socket_abstract = true; - socket_pathname = false; - }; -} diff --git a/test/sandbox/case/pdlike.nix b/test/sandbox/case/pdlike.nix deleted file mode 100644 index 7f0716fb..00000000 --- a/test/sandbox/case/pdlike.nix +++ /dev/null @@ -1,277 +0,0 @@ -{ - fs, - ent, - ignore, - system, -}: -let - extraPaths = { - x86_64-linux = { - fd = "fd0"; - "/dev/dri" = { - by-path = fs "800001ed" { - "pci-0000:00:09.0-card" = fs "80001ff" null null; - "pci-0000:00:09.0-render" = fs "80001ff" null null; - } null; - card0 = fs "42001b0" null null; - renderD128 = fs "42001b6" null null; - }; - sr = { - sr0 = fs "80001ff" null null; - }; - }; - aarch64-linux = { - fd = "mtdblock0"; - "/dev/dri" = null; - sr = { }; - }; - }; -in -{ - name = "pdlike"; - tty = true; - device = false; - mapRealUid = false; - useCommonPaths = false; - userns = true; - x11 = false; - hostAbstract = false; - shareRuntime = true; - shareTmpdir = true; - - # 0, PresetExt | PresetDenyDevel - expectedFilter = { - x86_64-linux = "c698b081ff957afe17a6d94374537d37f2a63f6f9dd75da7546542407a9e32476ebda3312ba7785d7f618542bcfaf27ca27dcc2dddba852069d28bcfe8cad39a"; - aarch64-linux = "433ce9b911282d6dcc8029319fb79b816b60d5a795ec8fc94344dd027614d68f023166a91bb881faaeeedd26e3d89474e141e5a69a97e93b8984ca8f14999980"; - }; - - want = { - env = [ - "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus" - "HOME=/var/lib/hakurei/u0/a5" - "SHELL=/run/current-system/sw/bin/bash" - "TERM=linux" - "USER=u0_a5" - "WAYLAND_DISPLAY=wayland-0" - "XDG_RUNTIME_DIR=/run/user/65534" - "XDG_SESSION_CLASS=user" - "XDG_SESSION_TYPE=wayland" - "PULSE_SERVER=unix:/run/user/65534/pulse/native" - ]; - - fs = fs "dead" { - ".hakurei" = fs "800001ed" { } null; - bin = fs "800001ed" { sh = fs "80001ff" null null; } null; - dev = fs "800001ed" { - console = fs "4200190" null null; - core = fs "80001ff" null null; - dri = fs "800001ed" extraPaths.${system}."/dev/dri" null; - fd = fs "80001ff" null null; - full = fs "42001b6" null null; - mqueue = fs "801001ff" { } null; - null = fs "42001b6" null ""; - ptmx = fs "80001ff" null null; - pts = fs "800001ed" { ptmx = fs "42001b6" null null; } null; - random = fs "42001b6" null null; - shm = fs "801001ff" { } null; - stderr = fs "80001ff" null null; - stdin = fs "80001ff" null null; - stdout = fs "80001ff" null null; - tty = fs "42001b6" null null; - urandom = fs "42001b6" null null; - zero = fs "42001b6" null null; - } null; - etc = fs "800001ed" { - ".clean" = fs "80001ff" null null; - ".host" = fs "800001c0" null null; - ".updated" = fs "80001ff" null null; - "NIXOS" = fs "80001ff" null null; - "X11" = fs "80001ff" null null; - "alsa" = fs "80001ff" null null; - "bash_logout" = fs "80001ff" null null; - "bashrc" = fs "80001ff" null null; - "binfmt.d" = fs "80001ff" null null; - "dbus-1" = fs "80001ff" null null; - "default" = fs "80001ff" null null; - "dhcpcd.exit-hook" = fs "80001ff" null null; - "environment.d" = fs "80001ff" null null; - "fonts" = fs "80001ff" null null; - "fstab" = fs "80001ff" null null; - "hsurc" = fs "80001ff" null null; - "fuse.conf" = fs "80001ff" null null; - "gai.conf" = fs "80001ff" null null; - "group" = fs "180" null "hakurei:x:65534:\n"; - "host.conf" = fs "80001ff" null null; - "hostname" = fs "80001ff" null null; - "hosts" = fs "80001ff" null null; - "inputrc" = fs "80001ff" null null; - "issue" = fs "80001ff" null null; - "kbd" = fs "80001ff" null null; - "locale.conf" = fs "80001ff" null null; - "login.defs" = fs "80001ff" null null; - "lsb-release" = fs "80001ff" null null; - "lvm" = fs "80001ff" null null; - "machine-id" = fs "80001ff" null null; - "man_db.conf" = fs "80001ff" null null; - "modprobe.d" = fs "80001ff" null null; - "modules-load.d" = fs "80001ff" null null; - "mtab" = fs "80001ff" null null; - "nanorc" = fs "80001ff" null null; - "netgroup" = fs "80001ff" null null; - "nix" = fs "80001ff" null null; - "nixos" = fs "80001ff" null null; - "nscd.conf" = fs "80001ff" null null; - "nsswitch.conf" = fs "80001ff" null null; - "os-release" = fs "80001ff" null null; - "pam" = fs "80001ff" null null; - "pam.d" = fs "80001ff" null null; - "passwd" = fs "180" null "u0_a5:x:65534:65534:Hakurei:/var/lib/hakurei/u0/a5:/run/current-system/sw/bin/bash\n"; - "pipewire" = fs "80001ff" null null; - "pki" = fs "80001ff" null null; - "polkit-1" = fs "80001ff" null null; - "profile" = fs "80001ff" null null; - "protocols" = fs "80001ff" null null; - "resolv.conf" = fs "80001ff" null null; - "resolvconf.conf" = fs "80001ff" null null; - "rpc" = fs "80001ff" null null; - "services" = fs "80001ff" null null; - "set-environment" = fs "80001ff" null null; - "shadow" = fs "80001ff" null null; - "shells" = fs "80001ff" null null; - "speech-dispatcher" = fs "80001ff" null null; - "ssh" = fs "80001ff" null null; - "ssl" = fs "80001ff" null null; - "static" = fs "80001ff" null null; - "subgid" = fs "80001ff" null null; - "subuid" = fs "80001ff" null null; - "sudoers" = fs "80001ff" null null; - "sway" = fs "80001ff" null null; - "sysctl.d" = fs "80001ff" null null; - "systemd" = fs "80001ff" null null; - "terminfo" = fs "80001ff" null null; - "tmpfiles.d" = fs "80001ff" null null; - "udev" = fs "80001ff" null null; - "vconsole.conf" = fs "80001ff" null null; - "xdg" = fs "80001ff" null null; - "zoneinfo" = fs "80001ff" null null; - } null; - nix = fs "800001c0" { store = fs "801001fd" null null; } null; - proc = fs "8000016d" null null; - run = fs "800001ed" { - current-system = fs "80001ff" null null; - opengl-driver = fs "80001ff" null null; - user = fs "800001ed" { - "65534" = fs "800001f8" { - bus = fs "10001fd" null null; - pulse = fs "800001c0" { native = fs "10001ff" null null; } null; - wayland-0 = fs "1000038" null null; - } null; - } null; - } null; - sys = fs "800001c0" { - block = fs "800001ed" ( - { - ${extraPaths.${system}.fd} = fs "80001ff" null null; - loop0 = fs "80001ff" null null; - loop1 = fs "80001ff" null null; - loop2 = fs "80001ff" null null; - loop3 = fs "80001ff" null null; - loop4 = fs "80001ff" null null; - loop5 = fs "80001ff" null null; - loop6 = fs "80001ff" null null; - loop7 = fs "80001ff" null null; - vda = fs "80001ff" null null; - } - // extraPaths.${system}.sr - ) null; - bus = fs "800001ed" null null; - class = fs "800001ed" null null; - dev = fs "800001ed" { - block = fs "800001ed" null null; - char = fs "800001ed" null null; - } null; - devices = fs "800001ed" null null; - } null; - tmp = fs "800001f8" { } null; - usr = fs "800001c0" { bin = fs "800001ed" { env = fs "80001ff" null null; } null; } null; - var = fs "800001c0" { - tmp = fs "801001ff" null null; - lib = fs "800001c0" { - hakurei = fs "800001c0" { - u0 = fs "800001c0" { - a5 = fs "800001c0" { - ".cache" = fs "800001ed" { ".keep" = fs "80001ff" null ""; } null; - ".config" = fs "800001ed" { - "environment.d" = fs "800001ed" { "10-home-manager.conf" = fs "80001ff" null null; } null; - systemd = fs "800001ed" { - user = fs "800001ed" { "tray.target" = fs "80001ff" null null; } null; - } null; - } null; - ".local" = fs "800001ed" { - state = fs "800001ed" { - ".keep" = fs "80001ff" null ""; - home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null; - nix = fs "800001ed" { - profiles = fs "800001ed" { - profile = fs "80001ff" null null; - profile-1-link = fs "80001ff" null null; - } null; - } null; - } null; - } null; - ".nix-defexpr" = fs "800001ed" { - channels = fs "80001ff" null null; - channels_root = fs "80001ff" null null; - } null; - ".nix-profile" = fs "80001ff" null null; - } null; - } null; - } null; - } null; - } null; - } null; - - mount = [ - (ent "/sysroot" "/" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10005,gid=10005") - (ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw") - (ent "/" "/.hakurei" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=10005,gid=10005") - (ent "/" "/dev" "ro,nosuid,nodev,relatime" "tmpfs" "devtmpfs" "rw,mode=755,uid=10005,gid=10005") - (ent "/null" "/dev/null" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/zero" "/dev/zero" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/full" "/dev/full" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/random" "/dev/random" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/urandom" "/dev/urandom" "rw,nosuid" "devtmpfs" "devtmpfs" 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 ignore "/dev/console" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666") - (ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw") - (ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10005,gid=10005") - (ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10005,gid=10005") - (ent "/tmp/hakurei.0/runtime/5" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/tmp/hakurei.0/tmpdir/5" "/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10005,gid=10005") - (ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10005,gid=10005") - (ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/sysroot/nix/.ro-store,upperdir=/sysroot/nix/.rw-store/upper,workdir=/sysroot/nix/.rw-store/work,uuid=on") - (ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/class" "/sys/class" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/var/lib/hakurei/u0/a5" "/var/lib/hakurei/u0/a5" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - ]; - - seccomp = true; - - try_socket = "/tmp/.X11-unix/X0"; - socket_abstract = false; - socket_pathname = false; - }; -} diff --git a/test/sandbox/case/preset.nix b/test/sandbox/case/preset.nix deleted file mode 100644 index 33cc3b8b..00000000 --- a/test/sandbox/case/preset.nix +++ /dev/null @@ -1,274 +0,0 @@ -{ - fs, - ent, - ignore, - system, -}: -let - extraPaths = { - x86_64-linux = { - fd = "fd0"; - "/dev/dri" = { - by-path = fs "800001ed" { - "pci-0000:00:09.0-card" = fs "80001ff" null null; - "pci-0000:00:09.0-render" = fs "80001ff" null null; - } null; - card0 = fs "42001b0" null null; - renderD128 = fs "42001b6" null null; - }; - sr = { - sr0 = fs "80001ff" null null; - }; - }; - aarch64-linux = { - fd = "mtdblock0"; - "/dev/dri" = null; - sr = { }; - }; - }; -in -{ - name = "preset"; - tty = false; - device = false; - mapRealUid = false; - useCommonPaths = false; - userns = false; - x11 = false; - hostAbstract = false; - shareRuntime = false; - shareTmpdir = false; - - # 0, PresetStrict - expectedFilter = { - x86_64-linux = "e880298df2bd6751d0040fc21bc0ed4c00f95dc0d7ba506c244d8b8cf6866dba8ef4a33296f287b66cccc1d78e97026597f84cc7dec1573e148960fbd35cd735"; - aarch64-linux = "79318538a3dc851314b6bd96f10d5861acb2aa7e13cb8de0619d0f6a76709d67f01ef3fd67e195862b02f9711e5b769bc4d1eb4fc0dfc41a723c89c968a93297"; - }; - - want = { - env = [ - "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus" - "HOME=/var/lib/hakurei/u0/a1" - "SHELL=/run/current-system/sw/bin/bash" - "TERM=linux" - "USER=u0_a1" - "WAYLAND_DISPLAY=wayland-0" - "XDG_RUNTIME_DIR=/run/user/65534" - "XDG_SESSION_CLASS=user" - "XDG_SESSION_TYPE=wayland" - "PULSE_SERVER=unix:/run/user/65534/pulse/native" - ]; - - fs = fs "dead" { - ".hakurei" = fs "800001ed" { } null; - bin = fs "800001ed" { sh = fs "80001ff" null null; } null; - dev = fs "800001ed" { - core = fs "80001ff" null null; - dri = fs "800001ed" extraPaths.${system}."/dev/dri" null; - fd = fs "80001ff" null null; - full = fs "42001b6" null null; - mqueue = fs "801001ff" { } null; - null = fs "42001b6" null ""; - ptmx = fs "80001ff" null null; - pts = fs "800001ed" { ptmx = fs "42001b6" null null; } null; - random = fs "42001b6" null null; - shm = fs "801001ff" { } null; - stderr = fs "80001ff" null null; - stdin = fs "80001ff" null null; - stdout = fs "80001ff" null null; - tty = fs "42001b6" null null; - urandom = fs "42001b6" null null; - zero = fs "42001b6" null null; - } null; - etc = fs "800001ed" { - ".clean" = fs "80001ff" null null; - ".host" = fs "800001c0" null null; - ".updated" = fs "80001ff" null null; - "NIXOS" = fs "80001ff" null null; - "X11" = fs "80001ff" null null; - "alsa" = fs "80001ff" null null; - "bash_logout" = fs "80001ff" null null; - "bashrc" = fs "80001ff" null null; - "binfmt.d" = fs "80001ff" null null; - "dbus-1" = fs "80001ff" null null; - "default" = fs "80001ff" null null; - "dhcpcd.exit-hook" = fs "80001ff" null null; - "environment.d" = fs "80001ff" null null; - "fonts" = fs "80001ff" null null; - "fstab" = fs "80001ff" null null; - "hsurc" = fs "80001ff" null null; - "fuse.conf" = fs "80001ff" null null; - "gai.conf" = fs "80001ff" null null; - "group" = fs "180" null "hakurei:x:65534:\n"; - "host.conf" = fs "80001ff" null null; - "hostname" = fs "80001ff" null null; - "hosts" = fs "80001ff" null null; - "inputrc" = fs "80001ff" null null; - "issue" = fs "80001ff" null null; - "kbd" = fs "80001ff" null null; - "locale.conf" = fs "80001ff" null null; - "login.defs" = fs "80001ff" null null; - "lsb-release" = fs "80001ff" null null; - "lvm" = fs "80001ff" null null; - "machine-id" = fs "80001ff" null null; - "man_db.conf" = fs "80001ff" null null; - "modprobe.d" = fs "80001ff" null null; - "modules-load.d" = fs "80001ff" null null; - "mtab" = fs "80001ff" null null; - "nanorc" = fs "80001ff" null null; - "netgroup" = fs "80001ff" null null; - "nix" = fs "80001ff" null null; - "nixos" = fs "80001ff" null null; - "nscd.conf" = fs "80001ff" null null; - "nsswitch.conf" = fs "80001ff" null null; - "os-release" = fs "80001ff" null null; - "pam" = fs "80001ff" null null; - "pam.d" = fs "80001ff" null null; - "passwd" = fs "180" null "u0_a1:x:65534:65534:Hakurei:/var/lib/hakurei/u0/a1:/run/current-system/sw/bin/bash\n"; - "pipewire" = fs "80001ff" null null; - "pki" = fs "80001ff" null null; - "polkit-1" = fs "80001ff" null null; - "profile" = fs "80001ff" null null; - "protocols" = fs "80001ff" null null; - "resolv.conf" = fs "80001ff" null null; - "resolvconf.conf" = fs "80001ff" null null; - "rpc" = fs "80001ff" null null; - "services" = fs "80001ff" null null; - "set-environment" = fs "80001ff" null null; - "shadow" = fs "80001ff" null null; - "shells" = fs "80001ff" null null; - "speech-dispatcher" = fs "80001ff" null null; - "ssh" = fs "80001ff" null null; - "ssl" = fs "80001ff" null null; - "static" = fs "80001ff" null null; - "subgid" = fs "80001ff" null null; - "subuid" = fs "80001ff" null null; - "sudoers" = fs "80001ff" null null; - "sway" = fs "80001ff" null null; - "sysctl.d" = fs "80001ff" null null; - "systemd" = fs "80001ff" null null; - "terminfo" = fs "80001ff" null null; - "tmpfiles.d" = fs "80001ff" null null; - "udev" = fs "80001ff" null null; - "vconsole.conf" = fs "80001ff" null null; - "xdg" = fs "80001ff" null null; - "zoneinfo" = fs "80001ff" null null; - } null; - nix = fs "800001c0" { store = fs "801001fd" null null; } null; - proc = fs "8000016d" null null; - run = fs "800001ed" { - current-system = fs "80001ff" null null; - opengl-driver = fs "80001ff" null null; - user = fs "800001ed" { - "65534" = fs "800001c0" { - bus = fs "10001fd" null null; - pulse = fs "800001c0" { native = fs "10001ff" null null; } null; - wayland-0 = fs "1000038" null null; - } null; - } null; - } null; - sys = fs "800001c0" { - block = fs "800001ed" ( - { - ${extraPaths.${system}.fd} = fs "80001ff" null null; - loop0 = fs "80001ff" null null; - loop1 = fs "80001ff" null null; - loop2 = fs "80001ff" null null; - loop3 = fs "80001ff" null null; - loop4 = fs "80001ff" null null; - loop5 = fs "80001ff" null null; - loop6 = fs "80001ff" null null; - loop7 = fs "80001ff" null null; - vda = fs "80001ff" null null; - } - // extraPaths.${system}.sr - ) null; - bus = fs "800001ed" null null; - class = fs "800001ed" null null; - dev = fs "800001ed" { - block = fs "800001ed" null null; - char = fs "800001ed" null null; - } null; - devices = fs "800001ed" null null; - } null; - tmp = fs "801001ff" { } null; - usr = fs "800001c0" { bin = fs "800001ed" { env = fs "80001ff" null null; } null; } null; - var = fs "800001c0" { - tmp = fs "801001ff" null null; - lib = fs "800001c0" { - hakurei = fs "800001c0" { - u0 = fs "800001c0" { - a1 = fs "800001c0" { - ".cache" = fs "800001ed" { ".keep" = fs "80001ff" null ""; } null; - ".config" = fs "800001ed" { - "environment.d" = fs "800001ed" { "10-home-manager.conf" = fs "80001ff" null null; } null; - systemd = fs "800001ed" { - user = fs "800001ed" { "tray.target" = fs "80001ff" null null; } null; - } null; - } null; - ".local" = fs "800001ed" { - state = fs "800001ed" { - ".keep" = fs "80001ff" null ""; - home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null; - nix = fs "800001ed" { - profiles = fs "800001ed" { - profile = fs "80001ff" null null; - profile-1-link = fs "80001ff" null null; - } null; - } null; - } null; - } null; - ".nix-defexpr" = fs "800001ed" { - channels = fs "80001ff" null null; - channels_root = fs "80001ff" null null; - } null; - ".nix-profile" = fs "80001ff" null null; - } null; - } null; - } null; - } null; - } null; - } null; - - mount = [ - (ent "/sysroot" "/" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10001,gid=10001") - (ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw") - (ent "/" "/.hakurei" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=10001,gid=10001") - (ent "/" "/dev" "ro,nosuid,nodev,relatime" "tmpfs" "devtmpfs" "rw,mode=755,uid=10001,gid=10001") - (ent "/null" "/dev/null" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/zero" "/dev/zero" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/full" "/dev/full" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/random" "/dev/random" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/urandom" "/dev/urandom" "rw,nosuid" "devtmpfs" "devtmpfs" 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/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw") - (ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10001,gid=10001") - (ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10001,gid=10001") - (ent "/" "/tmp" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10001,gid=10001") - (ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10001,gid=10001") - (ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10001,gid=10001") - (ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/sysroot/nix/.ro-store,upperdir=/sysroot/nix/.rw-store/upper,workdir=/sysroot/nix/.rw-store/work,uuid=on") - (ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/class" "/sys/class" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/var/lib/hakurei/u0/a1" "/var/lib/hakurei/u0/a1" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - ]; - - seccomp = true; - - try_socket = "/tmp/.X11-unix/X0"; - socket_abstract = false; - socket_pathname = false; - }; -} diff --git a/test/sandbox/case/tty.nix b/test/sandbox/case/tty.nix deleted file mode 100644 index 4ba9360e..00000000 --- a/test/sandbox/case/tty.nix +++ /dev/null @@ -1,288 +0,0 @@ -{ - fs, - ent, - ignore, - system, -}: -let - extraPaths = { - x86_64-linux = { - fd = "fd0"; - "/dev/dri" = { - by-path = fs "800001ed" { - "pci-0000:00:09.0-card" = fs "80001ff" null null; - "pci-0000:00:09.0-render" = fs "80001ff" null null; - } null; - card0 = fs "42001b0" null null; - renderD128 = fs "42001b6" null null; - }; - sr = { - sr0 = fs "80001ff" null null; - }; - }; - aarch64-linux = { - fd = "mtdblock0"; - "/dev/dri" = null; - sr = { }; - }; - }; -in -{ - name = "tty"; - tty = true; - device = false; - mapRealUid = false; - useCommonPaths = true; - userns = false; - x11 = true; - hostAbstract = true; - shareRuntime = true; - shareTmpdir = false; - - # 0, PresetExt | PresetDenyNS | PresetDenyDevel - expectedFilter = { - x86_64-linux = "0b76007476c1c9e25dbf674c29fdf609a1656a70063e49327654e1b5360ad3da06e1a3e32bf80e961c5516ad83d4b9e7e9bde876a93797e27627d2555c25858b"; - aarch64-linux = "cf1f4dc87436ba8ec95d268b663a6397bb0b4a5ac64d8557e6cc529d8b0f6f65dad3a92b62ed29d85eee9c6dde1267757a4d0f86032e8a45ca1bceadfa34cf5e"; - }; - - want = { - env = [ - "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65534/bus" - "DISPLAY=:0" - "HOME=/var/lib/hakurei/u0/a2" - "SHELL=/run/current-system/sw/bin/bash" - "TERM=linux" - "USER=u0_a2" - "WAYLAND_DISPLAY=wayland-0" - "XDG_RUNTIME_DIR=/run/user/65534" - "XDG_SESSION_CLASS=user" - "XDG_SESSION_TYPE=wayland" - "PULSE_SERVER=unix:/run/user/65534/pulse/native" - ]; - - fs = fs "dead" { - ".hakurei" = fs "800001ed" { - ".ro-store" = fs "801001fd" null null; - store = fs "800001ff" null null; - } null; - bin = fs "800001ed" { sh = fs "80001ff" null null; } null; - dev = fs "800001ed" { - console = fs "4200190" null null; - core = fs "80001ff" null null; - dri = fs "800001ed" extraPaths.${system}."/dev/dri" null; - fd = fs "80001ff" null null; - full = fs "42001b6" null null; - mqueue = fs "801001ff" { } null; - null = fs "42001b6" null ""; - ptmx = fs "80001ff" null null; - pts = fs "800001ed" { ptmx = fs "42001b6" null null; } null; - random = fs "42001b6" null null; - shm = fs "801001ff" { } null; - stderr = fs "80001ff" null null; - stdin = fs "80001ff" null null; - stdout = fs "80001ff" null null; - tty = fs "42001b6" null null; - urandom = fs "42001b6" null null; - zero = fs "42001b6" null null; - } null; - etc = fs "800001ed" { - ".clean" = fs "80001ff" null null; - ".host" = fs "800001c0" null null; - ".updated" = fs "80001ff" null null; - "NIXOS" = fs "80001ff" null null; - "X11" = fs "80001ff" null null; - "alsa" = fs "80001ff" null null; - "bash_logout" = fs "80001ff" null null; - "bashrc" = fs "80001ff" null null; - "binfmt.d" = fs "80001ff" null null; - "dbus-1" = fs "80001ff" null null; - "default" = fs "80001ff" null null; - "dhcpcd.exit-hook" = fs "80001ff" null null; - "environment.d" = fs "80001ff" null null; - "fonts" = fs "80001ff" null null; - "fstab" = fs "80001ff" null null; - "hsurc" = fs "80001ff" null null; - "fuse.conf" = fs "80001ff" null null; - "gai.conf" = fs "80001ff" null null; - "group" = fs "180" null "hakurei:x:65534:\n"; - "host.conf" = fs "80001ff" null null; - "hostname" = fs "80001ff" null null; - "hosts" = fs "80001ff" null null; - "inputrc" = fs "80001ff" null null; - "issue" = fs "80001ff" null null; - "kbd" = fs "80001ff" null null; - "locale.conf" = fs "80001ff" null null; - "login.defs" = fs "80001ff" null null; - "lsb-release" = fs "80001ff" null null; - "lvm" = fs "80001ff" null null; - "machine-id" = fs "80001ff" null null; - "man_db.conf" = fs "80001ff" null null; - "modprobe.d" = fs "80001ff" null null; - "modules-load.d" = fs "80001ff" null null; - "mtab" = fs "80001ff" null null; - "nanorc" = fs "80001ff" null null; - "netgroup" = fs "80001ff" null null; - "nix" = fs "80001ff" null null; - "nixos" = fs "80001ff" null null; - "nscd.conf" = fs "80001ff" null null; - "nsswitch.conf" = fs "80001ff" null null; - "os-release" = fs "80001ff" null null; - "pam" = fs "80001ff" null null; - "pam.d" = fs "80001ff" null null; - "passwd" = fs "180" null "u0_a2:x:65534:65534:Hakurei:/var/lib/hakurei/u0/a2:/run/current-system/sw/bin/bash\n"; - "pipewire" = fs "80001ff" null null; - "pki" = fs "80001ff" null null; - "polkit-1" = fs "80001ff" null null; - "profile" = fs "80001ff" null null; - "protocols" = fs "80001ff" null null; - "resolv.conf" = fs "80001ff" null null; - "resolvconf.conf" = fs "80001ff" null null; - "rpc" = fs "80001ff" null null; - "services" = fs "80001ff" null null; - "set-environment" = fs "80001ff" null null; - "shadow" = fs "80001ff" null null; - "shells" = fs "80001ff" null null; - "speech-dispatcher" = fs "80001ff" null null; - "ssh" = fs "80001ff" null null; - "ssl" = fs "80001ff" null null; - "static" = fs "80001ff" null null; - "subgid" = fs "80001ff" null null; - "subuid" = fs "80001ff" null null; - "sudoers" = fs "80001ff" null null; - "sway" = fs "80001ff" null null; - "sysctl.d" = fs "80001ff" null null; - "systemd" = fs "80001ff" null null; - "terminfo" = fs "80001ff" null null; - "tmpfiles.d" = fs "80001ff" null null; - "udev" = fs "80001ff" null null; - "vconsole.conf" = fs "80001ff" null null; - "xdg" = fs "80001ff" null null; - "zoneinfo" = fs "80001ff" null null; - } null; - nix = fs "800001c0" { store = fs "801001fd" null null; } null; - proc = fs "8000016d" null null; - run = fs "800001ed" { - current-system = fs "80001ff" null null; - opengl-driver = fs "80001ff" null null; - user = fs "800001ed" { - "65534" = fs "800001f8" { - bus = fs "10001fd" null null; - pulse = fs "800001c0" { native = fs "10001ff" null null; } null; - wayland-0 = fs "1000038" null null; - } null; - } null; - } null; - sys = fs "800001c0" { - block = fs "800001ed" ( - { - ${extraPaths.${system}.fd} = fs "80001ff" null null; - loop0 = fs "80001ff" null null; - loop1 = fs "80001ff" null null; - loop2 = fs "80001ff" null null; - loop3 = fs "80001ff" null null; - loop4 = fs "80001ff" null null; - loop5 = fs "80001ff" null null; - loop6 = fs "80001ff" null null; - loop7 = fs "80001ff" null null; - vda = fs "80001ff" null null; - } - // extraPaths.${system}.sr - ) null; - bus = fs "800001ed" null null; - class = fs "800001ed" null null; - dev = fs "800001ed" { - block = fs "800001ed" null null; - char = fs "800001ed" null null; - } null; - devices = fs "800001ed" null null; - } null; - tmp = fs "801001ff" { - ".X11-unix" = fs "801001ff" { X0 = fs "10001fd" null null; } null; - } null; - usr = fs "800001c0" { bin = fs "800001ed" { env = fs "80001ff" null null; } null; } null; - var = fs "800001c0" { - tmp = fs "801001ff" null null; - lib = fs "800001c0" { - hakurei = fs "800001c0" { - u0 = fs "800001c0" { - a2 = fs "800001c0" { - ".cache" = fs "800001ed" { ".keep" = fs "80001ff" null ""; } null; - ".config" = fs "800001ed" { - "environment.d" = fs "800001ed" { "10-home-manager.conf" = fs "80001ff" null null; } null; - systemd = fs "800001ed" { - user = fs "800001ed" { "tray.target" = fs "80001ff" null null; } null; - } null; - } null; - ".local" = fs "800001ed" { - state = fs "800001ed" { - ".keep" = fs "80001ff" null ""; - home-manager = fs "800001ed" { gcroots = fs "800001ed" { current-home = fs "80001ff" null null; } null; } null; - nix = fs "800001ed" { - profiles = fs "800001ed" { - profile = fs "80001ff" null null; - profile-1-link = fs "80001ff" null null; - } null; - } null; - } null; - } null; - ".nix-defexpr" = fs "800001ed" { - channels = fs "80001ff" null null; - channels_root = fs "80001ff" null null; - } null; - ".nix-profile" = fs "80001ff" null null; - } null; - } null; - } null; - } null; - cache = fs "800001ed" { private = fs "800001c0" null null; } null; - } null; - } null; - - mount = [ - (ent "/sysroot" "/" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10002,gid=10002") - (ent "/" "/proc" "rw,nosuid,nodev,noexec,relatime" "proc" "proc" "rw") - (ent "/" "/.hakurei" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=4k,mode=755,uid=10002,gid=10002") - (ent "/" "/dev" "ro,nosuid,nodev,relatime" "tmpfs" "devtmpfs" "rw,mode=755,uid=10002,gid=10002") - (ent "/null" "/dev/null" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/zero" "/dev/zero" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/full" "/dev/full" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/random" "/dev/random" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/urandom" "/dev/urandom" "rw,nosuid" "devtmpfs" "devtmpfs" 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 ignore "/dev/console" "rw,nosuid,noexec,relatime" "devpts" "devpts" "rw,gid=3,mode=620,ptmxmode=666") - (ent "/" "/dev/mqueue" "rw,nosuid,nodev,noexec,relatime" "mqueue" "mqueue" "rw") - (ent "/" "/dev/shm" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10002,gid=10002") - (ent "/" "/run/user" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,size=16384k,mode=755,uid=10002,gid=10002") - (ent "/tmp/hakurei.0/runtime/2" "/run/user/65534" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/" "/tmp" "rw,nosuid,nodev,relatime" "tmpfs" "ephemeral" "rw,uid=10002,gid=10002") - (ent ignore "/etc/passwd" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10002,gid=10002") - (ent ignore "/etc/group" "ro,nosuid,nodev,relatime" "tmpfs" "rootfs" "rw,uid=10002,gid=10002") - (ent ignore "/run/user/65534/wayland-0" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/tmp/.X11-unix" "/tmp/.X11-unix" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent ignore "/run/user/65534/bus" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/bin" "/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/usr/bin" "/usr/bin" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/" "/nix/store" "ro,nosuid,nodev,relatime" "overlay" "overlay" "rw,lowerdir=/sysroot/nix/.ro-store,upperdir=/sysroot/nix/.rw-store/upper,workdir=/sysroot/nix/.rw-store/work,uuid=on") - (ent "/block" "/sys/block" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/bus" "/sys/bus" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/class" "/sys/class" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/dev" "/sys/dev" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/devices" "/sys/devices" "ro,nosuid,nodev,noexec,relatime" "sysfs" "sysfs" "rw") - (ent "/dri" "/dev/dri" "rw,nosuid" "devtmpfs" "devtmpfs" ignore) - (ent "/var/tmp" "/var/tmp" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/var/cache" "/var/cache" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/" "/.hakurei/.ro-store" "rw,relatime" "overlay" "overlay" "ro,lowerdir+=/host/nix/.ro-store,lowerdir+=/host/nix/.rw-store/upper,redirect_dir=nofollow,userxattr") - (ent "/" "/.hakurei/store" "rw,relatime" "overlay" "overlay" "rw,lowerdir+=/host/nix/.ro-store,lowerdir+=/host/nix/.rw-store/upper,upperdir=/host/tmp/.hakurei-store-rw/upper,workdir=/host/tmp/.hakurei-store-rw/work,redirect_dir=nofollow,uuid=on,userxattr") - (ent "/etc" ignore "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent "/var/lib/hakurei/u0/a2" "/var/lib/hakurei/u0/a2" "rw,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - (ent ignore "/run/user/65534/pulse/native" "ro,nosuid,nodev,relatime" "ext4" "/dev/vda" "rw") - ]; - - seccomp = true; - - try_socket = "/tmp/.X11-unix/X0"; - socket_abstract = true; - socket_pathname = true; - }; -} diff --git a/test/sandbox/configuration.nix b/test/sandbox/configuration.nix deleted file mode 100644 index bc189f7d..00000000 --- a/test/sandbox/configuration.nix +++ /dev/null @@ -1,113 +0,0 @@ -{ - lib, - pkgs, - config, - ... -}: -let - testProgram = pkgs.callPackage ./tool/package.nix { inherit (config.environment.hakurei.package) version; }; - testCases = import ./case pkgs.stdenv.hostPlatform.system lib testProgram; -in -{ - 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 = { - systemPackages = [ - # For checking seccomp outcome: - testProgram - - # For checking pd outcome: - (pkgs.writeShellScriptBin "check-sandbox-pd" '' - hakurei -v exec hakurei-test \ - -p "/var/tmp/.hakurei-check-ok.0" \ - -t ${toString (builtins.toFile "hakurei-pd-want.json" (builtins.toJSON testCases.pd.want))} \ - -s ${testCases.pd.expectedFilter.${pkgs.stdenv.hostPlatform.system}} "$@" - '') - ]; - - 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.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 performance: - "-smp 8" - ]; - - environment.hakurei = { - enable = true; - stateDir = "/var/lib/hakurei"; - users.alice = 0; - - extraHomeConfig = { - home.stateVersion = "23.05"; - }; - - commonPaths = [ - { - type = "bind"; - src = "/var/tmp"; - write = true; - } - { - type = "bind"; - src = "/var/cache"; - write = true; - } - { - type = "overlay"; - dst = "/.hakurei/.ro-store"; - lower = [ - "/nix/.ro-store" - "/nix/.rw-store/upper" - ]; - } - { - type = "overlay"; - dst = "/.hakurei/store"; - lower = [ - "/nix/.ro-store" - "/nix/.rw-store/upper" - ]; - upper = "/tmp/.hakurei-store-rw/upper"; - work = "/tmp/.hakurei-store-rw/work"; - } - ]; - - inherit (testCases) apps; - }; -} diff --git a/test/sandbox/default.nix b/test/sandbox/default.nix deleted file mode 100644 index 47a59de9..00000000 --- a/test/sandbox/default.nix +++ /dev/null @@ -1,41 +0,0 @@ -{ - lib, - testers, - - self, - withRace ? false, -}: - -testers.nixosTest { - name = "hakurei-sandbox" + (if withRace then "-race" else ""); - nodes.machine = - { options, pkgs, ... }: - { - # Run with Go race detector: - environment.hakurei = lib.mkIf withRace rec { - # race detector does not support static linking - package = (pkgs.callPackage ../package.nix { }).overrideAttrs (previousAttrs: { - env = previousAttrs.env // { - GOFLAGS = previousAttrs.env.GOFLAGS + " -race"; - }; - }); - hsuPackage = options.environment.hakurei.hsuPackage.default.override { hakurei = package; }; - }; - - 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; -} diff --git a/test/sandbox/test.py b/test/sandbox/test.py deleted file mode 100644 index a431daab..00000000 --- a/test/sandbox/test.py +++ /dev/null @@ -1,88 +0,0 @@ -import json -import shlex - -q = shlex.quote - - -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 check_filter(check_offset, name, pname): - pid = int(machine.wait_until_succeeds(f"pgrep -U {10000+check_offset} -x {pname}")) - hash = machine.succeed(f"sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-1 check-sandbox-{name} hash") - print(machine.succeed(f"hakurei-test -s {hash} filter {pid}")) - - -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") - -# Check pd seccomp outcome: -swaymsg("exec hakurei exec cat") -check_filter(0, "pdlike", "cat") - -# Check fd leak: -swaymsg("exec exec 127</proc/cmdline && hakurei -v exec sleep infinity") -pd_identity0_sleep_pid = int(machine.wait_until_succeeds("pgrep -U 10000 -x sleep")) -print(machine.succeed(f"hakurei-test fd {pd_identity0_sleep_pid}")) -machine.succeed(f"kill -INT {pd_identity0_sleep_pid}") - -# Verify capabilities/securebits in user namespace: -print(machine.succeed("sudo -u alice -i hakurei exec capsh --print")) -print(machine.succeed("sudo -u alice -i hakurei exec capsh --has-no-new-privs")) -print(machine.fail("sudo -u alice -i hakurei exec capsh --has-a=CAP_SYS_ADMIN")) -print(machine.fail("sudo -u alice -i hakurei exec capsh --has-b=CAP_SYS_ADMIN")) -print(machine.fail("sudo -u alice -i hakurei exec capsh --has-i=CAP_SYS_ADMIN")) -print(machine.fail("sudo -u alice -i hakurei exec capsh --has-p=CAP_SYS_ADMIN")) -print(machine.fail("sudo -u alice -i hakurei exec umount -R /dev")) - -# Check sandbox outcome: -machine.succeed("install -dm0777 /tmp/.hakurei-store-rw/{upper,work}") -check_offset = 0 -def check_sandbox(name): - global check_offset - swaymsg(f"exec script /dev/null -E always -qec check-sandbox-{name}") - machine.wait_for_file(f"/var/tmp/.hakurei-check-ok.{check_offset}") - check_filter(check_offset, name, "hakurei-test") - check_offset += 1 - - -check_sandbox("pd") -check_sandbox("preset") -check_sandbox("tty") -check_sandbox("mapuid") -check_sandbox("device") -check_sandbox("pdlike") - -# Exit Sway and verify process exit status 0: -machine.wait_until_fails("pgrep -x hakurei") -swaymsg("exit", succeed=False) -machine.wait_for_file("/tmp/sway-exit-ok") - -# Print hakurei runDir contents: -print(machine.fail("ls /run/user/1000/hakurei")) -machine.succeed("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +") diff --git a/test/sandbox/tool/main.go b/test/sandbox/tool/main.go deleted file mode 100644 index 889142d4..00000000 --- a/test/sandbox/tool/main.go +++ /dev/null @@ -1,106 +0,0 @@ -//go:build testtool - -package main - -import ( - "flag" - "fmt" - "log" - "os" - "os/signal" - "strconv" - "strings" - "syscall" - - "hakurei.app/test/internal/sandbox" -) - -var ( - flagMarkerPath string - flagTestCase string - flagBpfHash string -) - -func init() { - flag.StringVar(&flagMarkerPath, "p", "/tmp/sandbox-ok", "Pathname of completion marker") - flag.StringVar(&flagTestCase, "t", "", "Nix store path to test case file") - flag.StringVar(&flagBpfHash, "s", "", "String representation of expected bpf sha512 hash") -} - -func main() { - log.SetFlags(0) - log.SetPrefix("test: ") - flag.Parse() - - args := flag.Args() - if len(args) < 1 { - s := make(chan os.Signal, 1) - signal.Notify(s, syscall.SIGINT) - go func() { <-s; log.Println("exiting on signal (likely from verifier)"); os.Exit(0) }() - - (&sandbox.T{FS: os.DirFS("/")}).MustCheckFile(flagTestCase) - if _, err := os.Create(flagMarkerPath); err != nil { - log.Fatalf("cannot create success marker: %v", err) - } - log.Printf("blocking for seccomp check (%s)", flagMarkerPath) - select {} - return - } - - switch args[0] { - case "filter": - if len(args) != 2 { - log.Fatal("invalid argument") - } - - if pid, err := strconv.Atoi(strings.TrimSpace(args[1])); err != nil { - log.Fatalf("%s", err) - } else if pid < 1 { - log.Fatalf("%d out of range", pid) - } else { - sandbox.MustCheckFilter(pid, flagBpfHash) - if err = syscall.Kill(pid, syscall.SIGINT); err != nil { - log.Fatalf("cannot signal check process: %v", err) - } - } - - case "hash": // this eases the pain of passing the hash to python - fmt.Print(flagBpfHash) - - case "fd": - if len(args) != 2 { - log.Fatal("invalid argument") - } - prefix := fmt.Sprintf("/proc/%s/fd/", args[1]) - - var fail bool - if entries, err := os.ReadDir(prefix); err != nil { - log.Fatal(err.Error()) - } else { - for _, ent := range entries { - var fd int - if fd, err = strconv.Atoi(ent.Name()); err != nil { - log.Fatal(err.Error()) - } - - // skip standard streams - if fd <= 2 { - continue - } - fail = true - - var d string - if d, err = os.Readlink(prefix + ent.Name()); err != nil { - log.Fatal(err.Error()) - } - log.Printf("[FAIL] extra fd %d -> %s", fd, d) - } - } - if fail { - log.Fatal("[FAIL] file descriptors leaked") - } - - default: - log.Fatal("invalid argument") - } -} diff --git a/test/sandbox/tool/package.nix b/test/sandbox/tool/package.nix deleted file mode 100644 index 13e18272..00000000 --- a/test/sandbox/tool/package.nix +++ /dev/null @@ -1,32 +0,0 @@ -{ - lib, - buildGoModule, - pkg-config, - util-linux, - - version, -}: -buildGoModule rec { - pname = "check-sandbox"; - inherit version; - - src = builtins.path { - name = "${pname}-src"; - path = lib.cleanSource ../../.; - filter = path: type: (type == "directory") || (type == "regular" && lib.hasSuffix ".go" path); - }; - vendorHash = null; - - tags = [ "testtool" ]; - - buildInputs = [ util-linux ]; - nativeBuildInputs = [ pkg-config ]; - - preBuild = '' - go mod init hakurei.app/test >& /dev/null - ''; - - postInstall = '' - mv $out/bin/tool $out/bin/hakurei-test - ''; -} diff --git a/test/sharefs/configuration.nix b/test/sharefs/configuration.nix deleted file mode 100644 index 05d67dcb..00000000 --- a/test/sharefs/configuration.nix +++ /dev/null @@ -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"; - }; - }; -} diff --git a/test/sharefs/default.nix b/test/sharefs/default.nix deleted file mode 100644 index e963eaa6..00000000 --- a/test/sharefs/default.nix +++ /dev/null @@ -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; -} diff --git a/test/sharefs/test.py b/test/sharefs/test.py deleted file mode 100644 index 4b925c9c..00000000 --- a/test/sharefs/test.py +++ /dev/null @@ -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") diff --git a/test/test.py b/test/test.py deleted file mode 100644 index 98f08275..00000000 --- a/test/test.py +++ /dev/null @@ -1,315 +0,0 @@ -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.wait_for_file(f"/tmp/{name}.ps") - machine.copy_from_vm(f"/tmp/{name}.ps", "") - swaymsg(f"exec hakurei --json ps > '/tmp/{name}.json'") - machine.wait_for_file(f"/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 = instances[0] - - command = f"{name}-start" - if not (instance['container']['path'].startswith("/nix/store/")) or not (instance['container']['path'].endswith(command)): - raise Exception(f"unexpected path {instance['path']}") - - if len(instance['container']['args']) != 1 or instance['container']['args'][0] != command: - raise Exception(f"unexpected args {instance['args']}") - - if instance['enablements'] != enablements: - raise Exception(f"unexpected enablements {instance['enablements']['enablements']}") - - -def hakurei(command): - swaymsg(f"exec hakurei {command}") - - -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") - -# Run hakurei Go tests outside of nix build in the background: -swaymsg("exec hakurei-test") - -# Deny unmapped uid: -denyOutput = machine.fail("sudo -u untrusted -i hakurei exec &>/dev/stdout") -print(denyOutput) -denyOutputVerbose = machine.fail("sudo -u untrusted -i hakurei -v exec &>/dev/stdout") -print(denyOutputVerbose) - -# Direct hsu call: -userid = machine.succeed("sudo -u alice -i hsu") -if userid != "0": - raise Exception(f"unexpected userid: {userid}") - -# Verify hsu fault behaviour: -if denyOutput != "hsu: uid 1001 is not in the hsurc file\n": - raise Exception(f"unexpected deny output:\n{denyOutput}") -if denyOutputVerbose != "hsu: uid 1001 is not in the hsurc file\nhakurei: *cannot retrieve user id from setuid wrapper: current user is not in the hsurc file\n": - raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}") - -# Verify timeout behaviour: -machine.succeed('sudo -u alice -i hakurei-check-linger-timeout > /var/tmp/linger-stdout 2> /var/tmp/linger-stderr || (cat /var/tmp/linger-stderr; false)') -linger_stdout = machine.succeed("cat /var/tmp/linger-stdout") -linger_stderr = machine.succeed("cat /var/tmp/linger-stderr") -if linger_stdout != "": - raise Exception(f"unexpected stdout: {linger_stdout}") -if linger_stderr != "init: timeout exceeded waiting for lingering processes\n": - raise Exception(f"unexpected stderr: {linger_stderr}") - -check_offset = 0 - - -def hakurei_identity(offset): - return 1+check_offset+offset - - -# Start hakurei permissive defaults outside Wayland session: -print(machine.succeed("sudo -u alice -i hakurei -v exec -a 0 touch /tmp/pd-bare-ok")) -machine.wait_for_file("/tmp/hakurei.0/tmpdir/0/pd-bare-ok") - -# Verify silent output permissive defaults: -output = machine.succeed("sudo -u alice -i hakurei exec -a 0 true &>/dev/stdout") -if output != "": - raise Exception(f"unexpected output\n{output}") - -# Verify silent output permissive defaults signal: -def silent_output_interrupt(flags): - swaymsg("exec foot") - wait_for_window("alice@machine") - # identity 0 does not have home-manager - machine.send_chars(f"exec hakurei exec {flags}-a 0 sh -c 'export PATH=/run/current-system/sw/bin:$PATH && touch /tmp/pd-silent-ready && sleep infinity' &>/tmp/pd-silent\n") - machine.wait_for_file("/tmp/hakurei.0/tmpdir/0/pd-silent-ready") - machine.succeed("rm /tmp/hakurei.0/tmpdir/0/pd-silent-ready") - machine.send_key("ctrl-c") - machine.wait_until_fails("pgrep foot") - machine.wait_until_fails(f"pgrep -u alice -f 'hakurei exec {flags}-a 0 '") - output = machine.succeed("cat /tmp/pd-silent && rm /tmp/pd-silent") - if output != "": - raise Exception(f"unexpected output\n{output}") - - -silent_output_interrupt("") -silent_output_interrupt("--dbus ") # this one is especially painful as it maintains a helper -silent_output_interrupt("--wayland -X --dbus --pulse ") - -# Verify graceful failure on bad Wayland display name: -print(machine.fail("sudo -u alice -i hakurei -v exec --wayland true")) - -# Start hakurei permissive defaults within Wayland session: -hakurei('-v exec --wayland --dbus --dbus-log notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-ok') -machine.wait_for_file("/tmp/dbus-ok") -collect_state_ui("dbus_notify_exited") -# not in pid namespace, verify termination -machine.wait_until_fails("pgrep xdg-dbus-proxy") -machine.succeed("pkill -9 mako") - -# Check revert type selection: -hakurei("-v exec --wayland -X --dbus --pulse -u p0 foot && touch /tmp/p0-exit-ok") -wait_for_window("p0@machine") -print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000")) -hakurei("-v exec --wayland -X --dbus --pulse -u p1 foot && touch /tmp/p1-exit-ok") -wait_for_window("p1@machine") -print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000")) -machine.send_chars("exit\n") -machine.wait_for_file("/tmp/p1-exit-ok") -# Verify acl is kept alive: -print(machine.succeed("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000")) -machine.send_chars("exit\n") -machine.wait_for_file("/tmp/p0-exit-ok") -machine.fail("getfacl --absolute-names --omit-header --numeric /tmp/hakurei.0/runtime | grep 10000") - -# Check invalid identifier fd behaviour: -machine.fail('echo \'{"container":{"shell":"/proc/nonexistent","home":"/proc/nonexistent","path":"/proc/nonexistent"}}\' | sudo -u alice -i hakurei -v run --identifier-fd 32767 - 2>&1 | tee > /tmp/invalid-identifier-fd') -machine.wait_for_file("/tmp/invalid-identifier-fd") -print(machine.succeed('grep "^hakurei: cannot write identifier: bad file descriptor$" /tmp/invalid-identifier-fd')) - -# Check interrupt shim behaviour: -swaymsg("exec sh -c 'ne-foot; echo -n $? > /tmp/monitor-exit-code'") -wait_for_window(f"u0_a{hakurei_identity(0)}@machine") -machine.succeed("pkill -INT -f 'hakurei -v run '") -machine.wait_until_fails("pgrep foot") -machine.wait_for_file("/tmp/monitor-exit-code") -interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code")) -if interrupt_exit_code != 230: - raise Exception(f"unexpected exit code {interrupt_exit_code}") - -# Check interrupt shim behaviour immediate termination: -swaymsg("exec sh -c 'ne-foot-immediate; echo -n $? > /tmp/monitor-exit-code'") -wait_for_window(f"u0_a{hakurei_identity(0)}@machine") -machine.succeed("pkill -INT -f 'hakurei -v run '") -machine.wait_until_fails("pgrep foot") -machine.wait_for_file("/tmp/monitor-exit-code") -interrupt_exit_code = int(machine.succeed("cat /tmp/monitor-exit-code")) -if interrupt_exit_code != 254: - raise Exception(f"unexpected exit code {interrupt_exit_code}") - -# Check shim SIGCONT from unexpected process behaviour: -swaymsg("exec sh -c 'ne-foot &> /tmp/shim-cont-unexpected-pid'") -wait_for_window(f"u0_a{hakurei_identity(0)}@machine") -machine.succeed("pkill -CONT -f 'hakurei shim'") -machine.succeed("pkill -INT -f 'hakurei -v run '") -machine.wait_until_fails("pgrep foot") -machine.wait_for_file("/tmp/shim-cont-unexpected-pid") -print(machine.succeed('grep "shim: got SIGCONT from unexpected process$" /tmp/shim-cont-unexpected-pid')) - -# Check setscheduler: -sched_unset = int(machine.succeed("sudo -u alice -i hakurei -v exec cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2")) -if sched_unset != 0: - raise Exception(f"unexpected unset policy: {sched_unset}") -sched_idle = int(machine.succeed("sudo -u alice -i hakurei -v exec --policy=idle cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2")) -if sched_idle != 5: - raise Exception(f"unexpected idle policy: {sched_idle}") -sched_rr = int(machine.succeed("sudo -u alice -i hakurei -v exec --policy=rr cat /proc/self/sched | grep '^policy' | tr -d ' ' | cut -d ':' -f 2")) -if sched_rr != 2: - raise Exception(f"unexpected round-robin policy: {sched_idle}") - -# Start app (foot) with Wayland enablement: -swaymsg("exec ne-foot") -wait_for_window(f"u0_a{hakurei_identity(0)}@machine") -machine.send_chars("clear; wayland-info && touch /var/tmp/client-ok\n") -machine.wait_for_file("/var/tmp/client-ok") -collect_state_ui("foot_wayland") -check_state("ne-foot", {"wayland": True}) -# Verify lack of acl on XDG_RUNTIME_DIR: -machine.fail(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {hakurei_identity(0) + 10000}") -machine.send_chars("exit\n") -machine.wait_until_fails("pgrep foot") -machine.fail(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {hakurei_identity(0) + 10000}") - -# Test pipewire-pulse: -swaymsg("exec pa-foot") -wait_for_window(f"u0_a{hakurei_identity(1)}@machine") -machine.send_chars("clear; pactl info && touch /var/tmp/pulse-ok\n") -machine.wait_for_file("/var/tmp/pulse-ok") -collect_state_ui("pulse_wayland") -check_state("pa-foot", {"wayland": True, "pipewire": True}) -machine.fail("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +") -machine.send_chars("exit\n") -machine.wait_until_fails("pgrep foot") -machine.wait_until_fails("pgrep -x hakurei") -machine.succeed("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +") -# Test PipeWire SecurityContext: -machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v exec --pulse pactl info") -machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v exec --pulse pactl set-sink-mute @DEFAULT_SINK@ toggle") -# Test PipeWire direct access: -machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 pw-dump") -machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei -v exec --pipewire pw-dump") - -# Test XWayland (foot does not support X): -swaymsg("exec x11-alacritty") -wait_for_window(f"u0_a{hakurei_identity(0)}@machine") -machine.send_chars("clear; glinfo && touch /var/tmp/x11-ok\n") -machine.wait_for_file("/var/tmp/x11-ok") -collect_state_ui("alacritty_x11") -check_state("x11-alacritty", {"x11": True}) -machine.send_chars("exit\n") -machine.wait_until_fails("pgrep alacritty") - -# Start app (foot) with direct Wayland access: -swaymsg("exec da-foot") -wait_for_window(f"u0_a{hakurei_identity(3)}@machine") -machine.send_chars("clear; wayland-info && touch /var/tmp/direct-ok\n") -collect_state_ui("foot_direct") -machine.wait_for_file("/var/tmp/direct-ok") -check_state("da-foot", {"wayland": True}) -# Verify acl on XDG_RUNTIME_DIR: -print(machine.succeed(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {hakurei_identity(3) + 10000}")) -machine.send_chars("exit\n") -machine.wait_until_fails("pgrep foot") -# Verify acl cleanup on XDG_RUNTIME_DIR: -machine.wait_until_fails(f"getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep {hakurei_identity(3) + 10000}") - -# Test syscall filter: -print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure")) - -# Start app (foot) with Wayland enablement from a terminal: -swaymsg("exec foot $SHELL -c '(ne-foot) & disown && exec $SHELL'") -wait_for_window(f"u0_a{hakurei_identity(0)}@machine") -machine.send_chars("clear; wayland-info && touch /var/tmp/term-ok\n") -machine.wait_for_file("/var/tmp/term-ok") -machine.send_key("alt-h") -machine.send_chars("clear; hakurei show $(hakurei ps --short) && touch /tmp/ps-show-ok && exec cat\n") -machine.wait_for_file("/tmp/ps-show-ok") -collect_state_ui("foot_wayland_term") -check_state("ne-foot", {"wayland": True}) -machine.send_key("alt-l") -machine.send_chars("exit\n") -wait_for_window("alice@machine") -machine.send_key("ctrl-c") -machine.wait_until_fails("pgrep foot") - -# Exit Sway and verify process exit status 0: -machine.wait_until_fails("pgrep -x hakurei") -swaymsg("exit", succeed=False) -machine.wait_for_file("/tmp/sway-exit-ok") - -# Print hakurei share and rundir contents: -print(machine.succeed("find /tmp/hakurei.0 " - + "-path '/tmp/hakurei.0/runtime/*/*' -prune -o " - + "-path '/tmp/hakurei.0/tmpdir/*/*' -prune -o " - + "-print")) -print(machine.succeed("find /run/user/1000/hakurei")) -machine.succeed("find /tmp -maxdepth 1 -type d -name '.hakurei-shim-*' -print -exec false '{}' +") - -# Verify go test status: -machine.wait_for_file("/tmp/hakurei-test-done") -print(machine.succeed("cat /tmp/hakurei-test.log")) -machine.wait_for_file("/tmp/hakurei-test-ok") |
