@synopackageland/cli 0.3.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -9
- package/dist/bin.js +1 -2
- package/dist/build-spk/generator.d.ts +0 -3
- package/dist/build-spk/generator.js +135 -287
- package/dist/build-spk/native-lifecycle.js +0 -3
- package/dist/build-spk/native-payload.js +0 -1
- package/dist/cli.js +171 -151
- package/dist/compose/analyze.d.ts +1 -2
- package/dist/compose/analyze.js +5 -48
- package/dist/contract/unsupported-types.d.ts +10 -0
- package/dist/contract/unsupported-types.js +49 -0
- package/dist/contract/validate.js +25 -2
- package/dist/contract/web-launch.js +0 -2
- package/dist/deploy/deploy.d.ts +1 -4
- package/dist/deploy/deploy.js +57 -156
- package/dist/deploy/homes-values.js +1 -1
- package/dist/deploy/plan.d.ts +18 -0
- package/dist/deploy/plan.js +54 -0
- package/dist/deploy/port-preflight.d.ts +21 -0
- package/dist/deploy/port-preflight.js +42 -0
- package/dist/deploy/spk-version.d.ts +2 -14
- package/dist/deploy/spk-version.js +25 -111
- package/dist/dev.d.ts +1 -3
- package/dist/dev.js +14 -28
- package/dist/error-registry.js +810 -857
- package/dist/host/dsm-client.js +23 -263
- package/dist/host/dsm-port-profile.d.ts +47 -0
- package/dist/host/dsm-port-profile.js +206 -0
- package/dist/host/port-info-lookup.d.ts +9 -0
- package/dist/host/port-info-lookup.js +87 -0
- package/dist/host/port-info.d.ts +33 -0
- package/dist/host/port-info.js +66 -0
- package/dist/host/types.d.ts +6 -78
- package/dist/index.d.ts +0 -9
- package/dist/index.js +0 -6
- package/dist/info.d.ts +0 -1
- package/dist/info.js +3 -10
- package/dist/init.d.ts +11 -1
- package/dist/init.js +112 -4
- package/dist/log.js +2 -3
- package/dist/native/validate.js +0 -1
- package/dist/standalone/binary.d.ts +9 -0
- package/dist/standalone/binary.js +67 -0
- package/dist/standalone/build.d.ts +25 -0
- package/dist/standalone/build.js +108 -0
- package/dist/standalone/dsm-window.d.ts +26 -0
- package/dist/standalone/dsm-window.js +251 -0
- package/dist/standalone/helper.d.ts +31 -0
- package/dist/standalone/helper.js +104 -0
- package/dist/standalone/i18n.d.ts +28 -0
- package/dist/standalone/i18n.js +161 -0
- package/dist/standalone/inspect-spk.d.ts +21 -0
- package/dist/standalone/inspect-spk.js +160 -0
- package/dist/standalone/manifest.d.ts +71 -0
- package/dist/standalone/manifest.js +276 -0
- package/dist/standalone/root-messages.d.ts +7 -0
- package/dist/standalone/root-messages.js +250 -0
- package/dist/standalone/root-ops.d.ts +42 -0
- package/dist/standalone/root-ops.js +101 -0
- package/dist/standalone/root-window.d.ts +35 -0
- package/dist/standalone/root-window.js +290 -0
- package/dist/standalone/spk.d.ts +22 -0
- package/dist/standalone/spk.js +205 -0
- package/dist/templates/binary/synology-app.yaml +7 -0
- package/dist/templates/hello-files/compose.yaml +0 -1
- package/dist/templates/hello-web/compose.yaml +13 -10
- package/dist/templates/hello-web/html/app.js +1 -23
- package/dist/templates/hello-web/html/index.html +1 -3
- package/dist/templates/hello-web-page/compose.yaml +13 -10
- package/dist/templates/hello-web-page/html/app.js +1 -23
- package/dist/templates/hello-web-page/html/index.html +1 -3
- package/dist/templates/native-go/README.md +268 -0
- package/dist/templates/native-go/backend/cmd/app/main.go +49 -0
- package/dist/templates/native-go/backend/cmd/helper/main.go +38 -0
- package/dist/templates/native-go/backend/go.mod +3 -0
- package/dist/templates/native-go/backend/internal/api/errors.go +33 -0
- package/dist/templates/native-go/backend/internal/api/registry.go +94 -0
- package/dist/templates/native-go/backend/internal/api/registry_test.go +329 -0
- package/dist/templates/native-go/backend/internal/api/server.go +155 -0
- package/dist/templates/native-go/backend/internal/app/root_none.go +9 -0
- package/dist/templates/native-go/backend/internal/app/routes.go +127 -0
- package/dist/templates/native-go/backend/internal/app/routes_test.go +167 -0
- package/dist/templates/native-go/backend/internal/dsm/auth.go +118 -0
- package/dist/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
- package/dist/templates/native-go/backend/internal/helper/control.go +219 -0
- package/dist/templates/native-go/backend/internal/helper/control_test.go +242 -0
- package/dist/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
- package/dist/templates/native-go/backend/internal/helper/peer_other.go +7 -0
- package/dist/templates/native-go/backend/internal/helper/process_linux.go +167 -0
- package/dist/templates/native-go/backend/internal/helper/process_other.go +5 -0
- package/dist/templates/native-go/backend/internal/helper/server_linux.go +83 -0
- package/dist/templates/native-go/backend/internal/helper/status.go +271 -0
- package/dist/templates/native-go/backend/internal/helper/status_test.go +205 -0
- package/dist/templates/native-go/backend/internal/helper/supervision.go +84 -0
- package/dist/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
- package/dist/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
- package/dist/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
- package/dist/templates/native-go/dsm-package.yaml +58 -0
- package/dist/templates/native-go/gitignore +1 -0
- package/dist/templates/native-go/i18n/glossary.json +662 -0
- package/dist/templates/native-go/i18n/messages/de.json +28 -0
- package/dist/templates/native-go/i18n/messages/en.json +28 -0
- package/dist/templates/native-go/i18n/messages/es.json +28 -0
- package/dist/templates/native-go/i18n/messages/fr.json +28 -0
- package/dist/templates/native-go/i18n/messages/it.json +28 -0
- package/dist/templates/native-go/i18n/messages/ja.json +28 -0
- package/dist/templates/native-go/i18n/messages/ko.json +28 -0
- package/dist/templates/native-go/i18n/messages/zh-CN.json +28 -0
- package/dist/templates/native-go/i18n/messages/zh-TW.json +28 -0
- package/dist/templates/native-go/ui/app.css +35 -0
- package/dist/templates/native-go/ui/app.js +94 -0
- package/dist/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
- package/dist/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
- package/dist/templates/native-go-better-auth/node/app.mjs +72 -0
- package/dist/templates/native-go-better-auth/node/build.mjs +2 -0
- package/dist/templates/native-go-better-auth/node/package-lock.json +862 -0
- package/dist/templates/native-go-better-auth/node/package.json +1 -0
- package/dist/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
- package/dist/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
- package/dist/templates/native-go-integration/backend/internal/app/personal.go +203 -0
- package/dist/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
- package/dist/templates/native-go-root/backend/internal/app/root.go +107 -0
- package/dist/templates/native-go-root/backend/internal/app/root_test.go +124 -0
- package/dist/templates/native-go-root/root/whoami.sh +6 -0
- package/dist/templates/native-go-vue/README.md +110 -0
- package/dist/templates/native-go-vue/frontend/DESIGN.md +78 -0
- package/dist/templates/native-go-vue/frontend/package-lock.json +2507 -0
- package/dist/templates/native-go-vue/frontend/package.json +6 -0
- package/dist/templates/native-go-vue/frontend/src/App.vue +132 -0
- package/dist/templates/native-go-vue/frontend/src/context.ts +17 -0
- package/dist/templates/native-go-vue/frontend/src/main.ts +26 -0
- package/dist/templates/native-go-vue/frontend/src/style.css +14 -0
- package/dist/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
- package/dist/templates/native-go-vue/frontend/tsconfig.json +1 -0
- package/dist/templates/native-go-vue/frontend/vite.config.ts +8 -0
- package/dist/templates/native-go-vue/gitignore +2 -0
- package/dist/types.d.ts +1 -1
- package/dist/validate.js +11 -7
- package/package.json +3 -7
- package/schema/synology-app.schema.json +36 -37
- package/skills/SKILL.md +4 -48
- package/skills/reference/create.md +11 -83
- package/skills/reference/deploy.md +7 -78
- package/skills/reference/diagnose.md +4 -101
- package/skills/reference/files.md +5 -41
- package/skills/reference/identity.md +3 -29
- package/skills/reference/wrap.md +0 -2
- package/templates/binary/synology-app.yaml +7 -0
- package/templates/hello-files/compose.yaml +0 -1
- package/templates/hello-web/compose.yaml +13 -10
- package/templates/hello-web/html/app.js +1 -23
- package/templates/hello-web/html/index.html +1 -3
- package/templates/hello-web-page/compose.yaml +13 -10
- package/templates/hello-web-page/html/app.js +1 -23
- package/templates/hello-web-page/html/index.html +1 -3
- package/templates/native-go/README.md +268 -0
- package/templates/native-go/backend/cmd/app/main.go +49 -0
- package/templates/native-go/backend/cmd/helper/main.go +38 -0
- package/templates/native-go/backend/go.mod +3 -0
- package/templates/native-go/backend/internal/api/errors.go +33 -0
- package/templates/native-go/backend/internal/api/registry.go +94 -0
- package/templates/native-go/backend/internal/api/registry_test.go +329 -0
- package/templates/native-go/backend/internal/api/server.go +155 -0
- package/templates/native-go/backend/internal/app/root_none.go +9 -0
- package/templates/native-go/backend/internal/app/routes.go +127 -0
- package/templates/native-go/backend/internal/app/routes_test.go +167 -0
- package/templates/native-go/backend/internal/dsm/auth.go +118 -0
- package/templates/native-go/backend/internal/dsm/auth_test.go +139 -0
- package/templates/native-go/backend/internal/helper/control.go +219 -0
- package/templates/native-go/backend/internal/helper/control_test.go +242 -0
- package/templates/native-go/backend/internal/helper/peer_linux.go +44 -0
- package/templates/native-go/backend/internal/helper/peer_other.go +7 -0
- package/templates/native-go/backend/internal/helper/process_linux.go +167 -0
- package/templates/native-go/backend/internal/helper/process_other.go +5 -0
- package/templates/native-go/backend/internal/helper/server_linux.go +83 -0
- package/templates/native-go/backend/internal/helper/status.go +271 -0
- package/templates/native-go/backend/internal/helper/status_test.go +205 -0
- package/templates/native-go/backend/internal/helper/supervision.go +84 -0
- package/templates/native-go/backend/internal/helper/supervision_test.go +124 -0
- package/templates/native-go/backend/internal/helper/supervisor_linux.go +192 -0
- package/templates/native-go/backend/internal/helper/supervisor_linux_test.go +302 -0
- package/templates/native-go/dsm-package.yaml +58 -0
- package/templates/native-go/gitignore +1 -0
- package/templates/native-go/i18n/glossary.json +662 -0
- package/templates/native-go/i18n/messages/de.json +28 -0
- package/templates/native-go/i18n/messages/en.json +28 -0
- package/templates/native-go/i18n/messages/es.json +28 -0
- package/templates/native-go/i18n/messages/fr.json +28 -0
- package/templates/native-go/i18n/messages/it.json +28 -0
- package/templates/native-go/i18n/messages/ja.json +28 -0
- package/templates/native-go/i18n/messages/ko.json +28 -0
- package/templates/native-go/i18n/messages/zh-CN.json +28 -0
- package/templates/native-go/i18n/messages/zh-TW.json +28 -0
- package/templates/native-go/ui/app.css +35 -0
- package/templates/native-go/ui/app.js +94 -0
- package/templates/native-go-better-auth/backend/internal/app/identity.go +208 -0
- package/templates/native-go-better-auth/backend/internal/app/identity_test.go +113 -0
- package/templates/native-go-better-auth/node/app.mjs +72 -0
- package/templates/native-go-better-auth/node/build.mjs +2 -0
- package/templates/native-go-better-auth/node/package-lock.json +862 -0
- package/templates/native-go-better-auth/node/package.json +1 -0
- package/templates/native-go-better-auth/node/test/auth.test.mjs +31 -0
- package/templates/native-go-integration/backend/internal/app/identity_none.go +6 -0
- package/templates/native-go-integration/backend/internal/app/personal.go +203 -0
- package/templates/native-go-integration/backend/internal/app/personal_test.go +119 -0
- package/templates/native-go-root/backend/internal/app/root.go +107 -0
- package/templates/native-go-root/backend/internal/app/root_test.go +124 -0
- package/templates/native-go-root/root/whoami.sh +6 -0
- package/templates/native-go-vue/README.md +110 -0
- package/templates/native-go-vue/frontend/DESIGN.md +78 -0
- package/templates/native-go-vue/frontend/package-lock.json +2507 -0
- package/templates/native-go-vue/frontend/package.json +6 -0
- package/templates/native-go-vue/frontend/src/App.vue +132 -0
- package/templates/native-go-vue/frontend/src/context.ts +17 -0
- package/templates/native-go-vue/frontend/src/main.ts +26 -0
- package/templates/native-go-vue/frontend/src/style.css +14 -0
- package/templates/native-go-vue/frontend/test/apps.test.ts +148 -0
- package/templates/native-go-vue/frontend/tsconfig.json +1 -0
- package/templates/native-go-vue/frontend/vite.config.ts +8 -0
- package/templates/native-go-vue/gitignore +2 -0
- package/dist/attestation/app-package.d.ts +0 -86
- package/dist/attestation/app-package.js +0 -206
- package/dist/attestation/install-on-host.d.ts +0 -14
- package/dist/attestation/install-on-host.js +0 -52
- package/dist/attestation/keyring.d.ts +0 -21
- package/dist/attestation/keyring.js +0 -168
- package/dist/attestation/platform-service.d.ts +0 -70
- package/dist/attestation/platform-service.js +0 -250
- package/dist/build-spk/broker-glue.d.ts +0 -16
- package/dist/build-spk/broker-glue.js +0 -40
- package/dist/deploy/catalog-publish.d.ts +0 -3
- package/dist/deploy/catalog-publish.js +0 -7
- package/dist/doctor.d.ts +0 -22
- package/dist/doctor.js +0 -721
- package/dist/error-collector.d.ts +0 -2
- package/dist/error-collector.js +0 -46
- package/dist/host/broker-paths.d.ts +0 -1
- package/dist/host/broker-paths.js +0 -1
- package/dist/host/broker-upgrade-preflight.d.ts +0 -61
- package/dist/host/broker-upgrade-preflight.js +0 -156
- package/dist/host/digests.d.ts +0 -5
- package/dist/host/digests.js +0 -63
- package/dist/host/fake-nas.d.ts +0 -41
- package/dist/host/fake-nas.js +0 -174
- package/dist/mock-broker/bootstrap.d.ts +0 -2
- package/dist/mock-broker/bootstrap.js +0 -8
- package/dist/mock-broker/files.d.ts +0 -60
- package/dist/mock-broker/files.js +0 -408
- package/dist/mock-broker/oidc.d.ts +0 -31
- package/dist/mock-broker/oidc.js +0 -161
- package/dist/mock-broker/path.d.ts +0 -9
- package/dist/mock-broker/path.js +0 -50
- package/dist/mock-broker/server.d.ts +0 -19
- package/dist/mock-broker/server.js +0 -350
- package/dist/publish/build-ledger.d.ts +0 -22
- package/dist/publish/build-ledger.js +0 -26
- package/dist/publish/community-path.d.ts +0 -6
- package/dist/publish/community-path.js +0 -41
- package/dist/publish/curated-release.d.ts +0 -78
- package/dist/publish/curated-release.js +0 -883
- package/dist/publish/file-build-ledger.d.ts +0 -18
- package/dist/publish/file-build-ledger.js +0 -268
- package/dist/publish/github-pr.d.ts +0 -25
- package/dist/publish/github-pr.js +0 -85
- package/dist/publish/github-repo.d.ts +0 -8
- package/dist/publish/github-repo.js +0 -21
- package/dist/publish/platform-service-release.d.ts +0 -60
- package/dist/publish/platform-service-release.js +0 -333
- package/dist/publish/preflight.d.ts +0 -24
- package/dist/publish/preflight.js +0 -138
- package/dist/publish/publish.d.ts +0 -27
- package/dist/publish/publish.js +0 -136
- package/dist/publish/sync-definition.d.ts +0 -4
- package/dist/publish/sync-definition.js +0 -48
- package/dist/release-bin.d.ts +0 -2
- package/dist/release-bin.js +0 -23
- package/dist/templates/hello-web/html/sdk.js +0 -68
- package/dist/templates/hello-web-page/html/sdk.js +0 -68
- package/dist/templates/native/synology-app.yaml +0 -7
- package/dist/test-host.d.ts +0 -15
- package/dist/test-host.js +0 -37
- package/templates/hello-web/html/sdk.js +0 -68
- package/templates/hello-web-page/html/sdk.js +0 -68
- package/templates/native/synology-app.yaml +0 -7
- /package/dist/templates/{native → binary}/native.yaml +0 -0
- /package/templates/{native → binary}/native.yaml +0 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// Package helper implements the opt-in per-package lifecycle contract. It has
|
|
2
|
+
// no file/grant/job API: all unimplemented operations fail closed.
|
|
3
|
+
package helper
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"context"
|
|
7
|
+
"crypto/rand"
|
|
8
|
+
"crypto/sha256"
|
|
9
|
+
"encoding/hex"
|
|
10
|
+
"encoding/json"
|
|
11
|
+
"errors"
|
|
12
|
+
"io"
|
|
13
|
+
"io/fs"
|
|
14
|
+
"net"
|
|
15
|
+
"os"
|
|
16
|
+
"path/filepath"
|
|
17
|
+
"regexp"
|
|
18
|
+
"syscall"
|
|
19
|
+
"time"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
type Inventory struct {
|
|
23
|
+
Package string `json:"package"`
|
|
24
|
+
Version string `json:"version"`
|
|
25
|
+
Protocol int `json:"protocol"`
|
|
26
|
+
PayloadDigest string `json:"payloadDigest"`
|
|
27
|
+
RegistryDigest string `json:"registryDigest"`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var digestPattern = regexp.MustCompile(`^[a-f0-9]{64}$`)
|
|
31
|
+
var packagePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9]{1,63}$`)
|
|
32
|
+
var versionPattern = regexp.MustCompile(`^\d+(\.\d+){0,3}-\d{4}$`)
|
|
33
|
+
|
|
34
|
+
func (i Inventory) Valid() bool {
|
|
35
|
+
return packagePattern.MatchString(i.Package) && versionPattern.MatchString(i.Version) && i.Protocol > 0 && digestPattern.MatchString(i.PayloadDigest) && digestPattern.MatchString(i.RegistryDigest)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type Record struct {
|
|
39
|
+
Inventory Inventory `json:"inventory"`
|
|
40
|
+
Enabled bool `json:"enabled"`
|
|
41
|
+
Residual bool `json:"residual,omitempty"`
|
|
42
|
+
}
|
|
43
|
+
type Request struct {
|
|
44
|
+
Inventory Inventory `json:"inventory"`
|
|
45
|
+
Op string `json:"op"`
|
|
46
|
+
Nonce string `json:"nonce"`
|
|
47
|
+
}
|
|
48
|
+
type Response struct {
|
|
49
|
+
Inventory Inventory `json:"inventory"`
|
|
50
|
+
Nonce string `json:"nonce"`
|
|
51
|
+
OK bool `json:"ok"`
|
|
52
|
+
Error string `json:"error,omitempty"`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func Respond(i Inventory, disabled bool, req Request) Response {
|
|
56
|
+
r := Response{Inventory: i, Nonce: req.Nonce}
|
|
57
|
+
switch {
|
|
58
|
+
case disabled:
|
|
59
|
+
r.Error = "disabled"
|
|
60
|
+
case !i.Valid() || req.Inventory != i:
|
|
61
|
+
r.Error = "incompatible"
|
|
62
|
+
case req.Op != "health":
|
|
63
|
+
r.Error = "unsupported"
|
|
64
|
+
default:
|
|
65
|
+
r.OK = true
|
|
66
|
+
}
|
|
67
|
+
return r
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// State is safe to expose to any authenticated DSM user. Paths, PIDs, worker
|
|
71
|
+
// internals, scheduler definitions and raw errors never enter this envelope.
|
|
72
|
+
type State struct {
|
|
73
|
+
State string `json:"state"`
|
|
74
|
+
Reason string `json:"reason"`
|
|
75
|
+
PackageRunning string `json:"packageRunning"`
|
|
76
|
+
Scheduler string `json:"scheduler"`
|
|
77
|
+
Cleanup string `json:"cleanup,omitempty"`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type Client struct {
|
|
81
|
+
Expected Inventory
|
|
82
|
+
StateDir, RunDir string
|
|
83
|
+
// Production uses UID 0 and RootPeer. Tests inject only local fixture identity.
|
|
84
|
+
Owner uint32
|
|
85
|
+
Peer func(*net.UnixConn) error
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func NewClient(i Inventory) Client {
|
|
89
|
+
return Client{Expected: i, StateDir: "/var/lib/synopkgland-" + i.Package, RunDir: "/run/synopkgland-helper-" + i.Package, Peer: RootPeer}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// LocalClient is a nonprivileged diagnostic entry point for package hooks.
|
|
93
|
+
// Hashing the local executable establishes only the expected build inventory;
|
|
94
|
+
// readiness still requires the protected record and root socket handshake.
|
|
95
|
+
func LocalClient(i Inventory) (Client, error) {
|
|
96
|
+
exe, err := os.Executable()
|
|
97
|
+
if err != nil {
|
|
98
|
+
return Client{}, err
|
|
99
|
+
}
|
|
100
|
+
f, err := os.Open(exe)
|
|
101
|
+
if err != nil {
|
|
102
|
+
return Client{}, err
|
|
103
|
+
}
|
|
104
|
+
defer f.Close()
|
|
105
|
+
h := sha256.New()
|
|
106
|
+
if _, err := io.Copy(h, io.LimitReader(f, (64<<20)+1)); err != nil {
|
|
107
|
+
return Client{}, err
|
|
108
|
+
}
|
|
109
|
+
i.PayloadDigest = hex.EncodeToString(h.Sum(nil))
|
|
110
|
+
if !i.Valid() {
|
|
111
|
+
return Client{}, errTrust
|
|
112
|
+
}
|
|
113
|
+
return NewClient(i), nil
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
func (c Client) CheckStopped(ctx context.Context) error {
|
|
117
|
+
s := c.Status(ctx)
|
|
118
|
+
if s.Cleanup != "" || (s.State != "not-enabled" && s.State != "stopped" && s.State != "needs-approval") {
|
|
119
|
+
return errors.New("approved helper teardown required")
|
|
120
|
+
}
|
|
121
|
+
for _, file := range []string{"process.json", "helper.sock", "supervisor.sock"} {
|
|
122
|
+
if _, err := os.Lstat(filepath.Join(c.RunDir, file)); !errors.Is(err, fs.ErrNotExist) {
|
|
123
|
+
return errors.New("helper residue; approved teardown required")
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return nil
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
var errTrust = errors.New("untrusted helper state")
|
|
130
|
+
|
|
131
|
+
func protected(path string, owner uint32, directory bool) error {
|
|
132
|
+
st, err := os.Lstat(path)
|
|
133
|
+
if err != nil {
|
|
134
|
+
return err
|
|
135
|
+
}
|
|
136
|
+
s, ok := st.Sys().(*syscall.Stat_t)
|
|
137
|
+
if !ok || s.Uid != owner || st.Mode().Perm()&0022 != 0 || (directory && !st.IsDir()) || (!directory && !st.Mode().IsRegular()) {
|
|
138
|
+
return errTrust
|
|
139
|
+
}
|
|
140
|
+
return nil
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func readRecord(dir string, owner uint32) (Record, error) {
|
|
144
|
+
var r Record
|
|
145
|
+
if err := protected(dir, owner, true); err != nil {
|
|
146
|
+
return r, err
|
|
147
|
+
}
|
|
148
|
+
f, err := os.OpenFile(filepath.Join(dir, "approval.json"), os.O_RDONLY|syscall.O_NOFOLLOW, 0)
|
|
149
|
+
if err != nil {
|
|
150
|
+
return r, err
|
|
151
|
+
}
|
|
152
|
+
defer f.Close()
|
|
153
|
+
st, err := f.Stat()
|
|
154
|
+
if err != nil {
|
|
155
|
+
return r, err
|
|
156
|
+
}
|
|
157
|
+
s, ok := st.Sys().(*syscall.Stat_t)
|
|
158
|
+
if !ok || s.Uid != owner || st.Mode().Perm()&0022 != 0 || !st.Mode().IsRegular() || st.Size() > 4096 {
|
|
159
|
+
return r, errTrust
|
|
160
|
+
}
|
|
161
|
+
d := json.NewDecoder(io.LimitReader(f, 4097))
|
|
162
|
+
d.DisallowUnknownFields()
|
|
163
|
+
var decoded struct {
|
|
164
|
+
Inventory Inventory `json:"inventory"`
|
|
165
|
+
Enabled *bool `json:"enabled"`
|
|
166
|
+
Residual bool `json:"residual,omitempty"`
|
|
167
|
+
}
|
|
168
|
+
if d.Decode(&decoded) != nil || d.Decode(new(any)) != io.EOF || !decoded.Inventory.Valid() || decoded.Enabled == nil {
|
|
169
|
+
return r, errTrust
|
|
170
|
+
}
|
|
171
|
+
r = Record{Inventory: decoded.Inventory, Enabled: *decoded.Enabled, Residual: decoded.Residual}
|
|
172
|
+
return r, nil
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
func (c Client) Status(ctx context.Context) State {
|
|
176
|
+
result := func(state, reason string) State {
|
|
177
|
+
return State{State: state, Reason: reason, PackageRunning: "unknown", Scheduler: "not-probed"}
|
|
178
|
+
}
|
|
179
|
+
if c.Expected.Package == "" {
|
|
180
|
+
return result("not-enabled", "not-requested")
|
|
181
|
+
}
|
|
182
|
+
if !c.Expected.Valid() {
|
|
183
|
+
return result("incompatible", "invalid-inventory")
|
|
184
|
+
}
|
|
185
|
+
r, err := readRecord(c.StateDir, c.Owner)
|
|
186
|
+
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
187
|
+
return result("incompatible", "untrusted-approval")
|
|
188
|
+
}
|
|
189
|
+
if err == nil && !r.Enabled {
|
|
190
|
+
s := result("not-enabled", "disabled")
|
|
191
|
+
if r.Residual {
|
|
192
|
+
s.Cleanup = "required"
|
|
193
|
+
}
|
|
194
|
+
return s
|
|
195
|
+
}
|
|
196
|
+
if err == nil && r.Inventory != c.Expected {
|
|
197
|
+
return result("incompatible", "inventory-mismatch")
|
|
198
|
+
}
|
|
199
|
+
if err == nil && r.Residual {
|
|
200
|
+
s := result("failed", "incomplete-transition")
|
|
201
|
+
s.Cleanup = "required"
|
|
202
|
+
return s
|
|
203
|
+
}
|
|
204
|
+
// Conflict has precedence over missing approval. Even an unapproved leftover
|
|
205
|
+
// socket is probed before claiming that authorization alone can fix it.
|
|
206
|
+
if e := protected(c.RunDir, c.Owner, true); e != nil && !errors.Is(e, fs.ErrNotExist) {
|
|
207
|
+
return result("incompatible", "untrusted-runtime")
|
|
208
|
+
}
|
|
209
|
+
socket := filepath.Join(c.RunDir, "helper.sock")
|
|
210
|
+
st, e := os.Lstat(socket)
|
|
211
|
+
if e == nil {
|
|
212
|
+
s, ok := st.Sys().(*syscall.Stat_t)
|
|
213
|
+
if !ok || s.Uid != c.Owner || st.Mode()&os.ModeSocket == 0 || st.Mode().Perm()&0002 != 0 {
|
|
214
|
+
return result("incompatible", "untrusted-socket")
|
|
215
|
+
}
|
|
216
|
+
state, reason := c.probe(ctx, socket)
|
|
217
|
+
if state != "ready" {
|
|
218
|
+
return result(state, reason)
|
|
219
|
+
}
|
|
220
|
+
} else if !errors.Is(e, fs.ErrNotExist) {
|
|
221
|
+
return result("failed", "runtime-unreadable")
|
|
222
|
+
}
|
|
223
|
+
if err != nil {
|
|
224
|
+
return result("needs-approval", "approval-missing")
|
|
225
|
+
}
|
|
226
|
+
if e != nil {
|
|
227
|
+
return result("stopped", "runtime-missing")
|
|
228
|
+
}
|
|
229
|
+
return result("ready", "live-handshake")
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
func (c Client) probe(ctx context.Context, socket string) (string, string) {
|
|
233
|
+
deadline := time.Now().Add(time.Second)
|
|
234
|
+
if d, ok := ctx.Deadline(); ok && d.Before(deadline) {
|
|
235
|
+
deadline = d
|
|
236
|
+
}
|
|
237
|
+
dialer := net.Dialer{Deadline: deadline}
|
|
238
|
+
conn, err := dialer.DialContext(ctx, "unix", socket)
|
|
239
|
+
if err != nil {
|
|
240
|
+
if errors.Is(err, syscall.ECONNREFUSED) || errors.Is(err, fs.ErrNotExist) {
|
|
241
|
+
return "stopped", "runtime-not-listening"
|
|
242
|
+
}
|
|
243
|
+
return "failed", "probe-failed"
|
|
244
|
+
}
|
|
245
|
+
defer conn.Close()
|
|
246
|
+
if c.Peer == nil || c.Peer(conn.(*net.UnixConn)) != nil {
|
|
247
|
+
return "incompatible", "untrusted-peer"
|
|
248
|
+
}
|
|
249
|
+
conn.SetDeadline(deadline)
|
|
250
|
+
var nonce [32]byte
|
|
251
|
+
if _, err := rand.Read(nonce[:]); err != nil {
|
|
252
|
+
return "failed", "probe-failed"
|
|
253
|
+
}
|
|
254
|
+
req := Request{c.Expected, "health", hex.EncodeToString(nonce[:])}
|
|
255
|
+
if json.NewEncoder(conn).Encode(req) != nil {
|
|
256
|
+
return "failed", "probe-failed"
|
|
257
|
+
}
|
|
258
|
+
var res Response
|
|
259
|
+
d := json.NewDecoder(io.LimitReader(conn, 4097))
|
|
260
|
+
d.DisallowUnknownFields()
|
|
261
|
+
if d.Decode(&res) != nil {
|
|
262
|
+
return "failed", "invalid-handshake"
|
|
263
|
+
}
|
|
264
|
+
if res.Nonce != req.Nonce || res.Inventory != c.Expected || res.Error == "incompatible" {
|
|
265
|
+
return "incompatible", "handshake-mismatch"
|
|
266
|
+
}
|
|
267
|
+
if !res.OK || res.Error != "" {
|
|
268
|
+
return "failed", "health-refused"
|
|
269
|
+
}
|
|
270
|
+
return "ready", "live-handshake"
|
|
271
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
package helper
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"net"
|
|
7
|
+
"os"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"strings"
|
|
10
|
+
"testing"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func fixture(t *testing.T) Client {
|
|
14
|
+
t.Helper()
|
|
15
|
+
i := Inventory{Package: "Fixture", Version: "1.0.0-0001", Protocol: 1, PayloadDigest: strings.Repeat("a", 64), RegistryDigest: strings.Repeat("b", 64)}
|
|
16
|
+
run, err := os.MkdirTemp("/tmp", "m1a-")
|
|
17
|
+
if err != nil {
|
|
18
|
+
t.Fatal(err)
|
|
19
|
+
}
|
|
20
|
+
t.Cleanup(func() { os.RemoveAll(run) })
|
|
21
|
+
return Client{Expected: i, StateDir: t.TempDir(), RunDir: run, Owner: uint32(os.Getuid()), Peer: func(*net.UnixConn) error { return nil }}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func record(t *testing.T, c Client, r Record) {
|
|
25
|
+
t.Helper()
|
|
26
|
+
b, _ := json.Marshal(r)
|
|
27
|
+
if err := os.WriteFile(filepath.Join(c.StateDir, "approval.json"), b, 0644); err != nil {
|
|
28
|
+
t.Fatal(err)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func live(t *testing.T, c Client, i Inventory, reply func(Request) Response) {
|
|
33
|
+
t.Helper()
|
|
34
|
+
l, err := net.ListenUnix("unix", &net.UnixAddr{Name: filepath.Join(c.RunDir, "helper.sock"), Net: "unix"})
|
|
35
|
+
if err != nil {
|
|
36
|
+
t.Fatal(err)
|
|
37
|
+
}
|
|
38
|
+
t.Cleanup(func() { l.Close() })
|
|
39
|
+
go func() {
|
|
40
|
+
for {
|
|
41
|
+
conn, err := l.AcceptUnix()
|
|
42
|
+
if err != nil {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
go func() {
|
|
46
|
+
defer conn.Close()
|
|
47
|
+
var req Request
|
|
48
|
+
if json.NewDecoder(conn).Decode(&req) != nil {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
r := Respond(i, false, req)
|
|
52
|
+
if reply != nil {
|
|
53
|
+
r = reply(req)
|
|
54
|
+
}
|
|
55
|
+
json.NewEncoder(conn).Encode(r)
|
|
56
|
+
}()
|
|
57
|
+
}
|
|
58
|
+
}()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func TestSixStatesAndPriority(t *testing.T) {
|
|
62
|
+
c := fixture(t)
|
|
63
|
+
check := func(want string) {
|
|
64
|
+
t.Helper()
|
|
65
|
+
s := c.Status(context.Background())
|
|
66
|
+
if s.State != want {
|
|
67
|
+
t.Fatalf("want %s, got %+v", want, s)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
c.Expected = Inventory{}
|
|
71
|
+
check("not-enabled")
|
|
72
|
+
c.Expected = fixture(t).Expected
|
|
73
|
+
check("needs-approval")
|
|
74
|
+
record(t, c, Record{Inventory: c.Expected, Enabled: true})
|
|
75
|
+
check("stopped")
|
|
76
|
+
live(t, c, c.Expected, nil)
|
|
77
|
+
check("ready")
|
|
78
|
+
old := c.Expected
|
|
79
|
+
old.Protocol++
|
|
80
|
+
record(t, c, Record{Inventory: old, Enabled: true})
|
|
81
|
+
check("incompatible")
|
|
82
|
+
record(t, c, Record{Inventory: old, Enabled: false})
|
|
83
|
+
check("not-enabled")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func TestLiveProbeCannotBeReplacedByApprovalOrHistoricalOutput(t *testing.T) {
|
|
87
|
+
c := fixture(t)
|
|
88
|
+
record(t, c, Record{Inventory: c.Expected, Enabled: true})
|
|
89
|
+
if err := os.WriteFile(filepath.Join(c.RunDir, "helper.sock"), []byte("ready"), 0644); err != nil {
|
|
90
|
+
t.Fatal(err)
|
|
91
|
+
}
|
|
92
|
+
if got := c.Status(context.Background()).State; got != "incompatible" {
|
|
93
|
+
t.Fatal(got)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func TestHandshakeFailuresAreRedacted(t *testing.T) {
|
|
98
|
+
for _, kind := range []string{"nonce", "protocol", "payload", "worker", "version", "peer"} {
|
|
99
|
+
t.Run(kind, func(t *testing.T) {
|
|
100
|
+
c := fixture(t)
|
|
101
|
+
record(t, c, Record{Inventory: c.Expected, Enabled: true})
|
|
102
|
+
live(t, c, c.Expected, func(req Request) Response {
|
|
103
|
+
r := Respond(c.Expected, false, req)
|
|
104
|
+
switch kind {
|
|
105
|
+
case "nonce":
|
|
106
|
+
r.Nonce = "replay"
|
|
107
|
+
case "protocol":
|
|
108
|
+
r.Inventory.Protocol++
|
|
109
|
+
case "payload":
|
|
110
|
+
r.Inventory.PayloadDigest = "foreign"
|
|
111
|
+
case "worker":
|
|
112
|
+
r.Inventory.RegistryDigest = "foreign"
|
|
113
|
+
case "version":
|
|
114
|
+
r.Inventory.Version = "old"
|
|
115
|
+
}
|
|
116
|
+
return r
|
|
117
|
+
})
|
|
118
|
+
if kind == "peer" {
|
|
119
|
+
c.Peer = func(*net.UnixConn) error { return os.ErrPermission }
|
|
120
|
+
}
|
|
121
|
+
s := c.Status(context.Background())
|
|
122
|
+
if s.State != "incompatible" {
|
|
123
|
+
t.Fatalf("%+v", s)
|
|
124
|
+
}
|
|
125
|
+
b, _ := json.Marshal(s)
|
|
126
|
+
if strings.Contains(string(b), c.StateDir) || strings.Contains(string(b), "foreign") {
|
|
127
|
+
t.Fatal(string(b))
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
c := fixture(t)
|
|
132
|
+
record(t, c, Record{Inventory: c.Expected, Enabled: true})
|
|
133
|
+
live(t, c, c.Expected, func(req Request) Response { return Response{Inventory: c.Expected, Nonce: req.Nonce, Error: "failed"} })
|
|
134
|
+
if c.Status(context.Background()).State != "failed" {
|
|
135
|
+
t.Fatal("health failure must be failed")
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func TestUntrustedPersistentStateFailsClosed(t *testing.T) {
|
|
140
|
+
for _, kind := range []string{"writable", "symlink", "malformed"} {
|
|
141
|
+
t.Run(kind, func(t *testing.T) {
|
|
142
|
+
c := fixture(t)
|
|
143
|
+
record(t, c, Record{Inventory: c.Expected, Enabled: true})
|
|
144
|
+
p := filepath.Join(c.StateDir, "approval.json")
|
|
145
|
+
switch kind {
|
|
146
|
+
case "writable":
|
|
147
|
+
os.Chmod(p, 0666)
|
|
148
|
+
case "symlink":
|
|
149
|
+
os.Rename(p, p+".real")
|
|
150
|
+
os.Symlink(p+".real", p)
|
|
151
|
+
case "malformed":
|
|
152
|
+
os.WriteFile(p, []byte(`{"enabled":false}`), 0644)
|
|
153
|
+
}
|
|
154
|
+
if c.Status(context.Background()).State != "incompatible" {
|
|
155
|
+
t.Fatal("untrusted state accepted")
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func TestOldHelperRejectsNewInventoryAndAllUnimplementedWork(t *testing.T) {
|
|
162
|
+
i := fixture(t).Expected
|
|
163
|
+
for _, field := range []string{"protocol", "payload", "workers", "version", "package"} {
|
|
164
|
+
new := i
|
|
165
|
+
switch field {
|
|
166
|
+
case "protocol":
|
|
167
|
+
new.Protocol++
|
|
168
|
+
case "payload":
|
|
169
|
+
new.PayloadDigest = strings.Repeat("c", 64)
|
|
170
|
+
case "workers":
|
|
171
|
+
new.RegistryDigest = strings.Repeat("c", 64)
|
|
172
|
+
case "version":
|
|
173
|
+
new.Version = "2.0.0-0001"
|
|
174
|
+
case "package":
|
|
175
|
+
new.Package = "Other"
|
|
176
|
+
}
|
|
177
|
+
r := Respond(i, false, Request{Inventory: new, Op: "work", Nonce: "test"})
|
|
178
|
+
if r.Error != "incompatible" || r.OK {
|
|
179
|
+
t.Fatalf("%s: %+v", field, r)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if r := Respond(i, true, Request{Inventory: i, Op: "health"}); r.OK || r.Error != "disabled" {
|
|
183
|
+
t.Fatal(r)
|
|
184
|
+
}
|
|
185
|
+
if r := Respond(i, false, Request{Inventory: i, Op: "work"}); r.OK || r.Error != "unsupported" {
|
|
186
|
+
t.Fatal(r)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
func TestMissingEnableFieldIsNotARevocationRecord(t *testing.T) {
|
|
191
|
+
c := fixture(t)
|
|
192
|
+
b, _ := json.Marshal(map[string]any{"inventory": c.Expected})
|
|
193
|
+
os.WriteFile(filepath.Join(c.StateDir, "approval.json"), b, 0644)
|
|
194
|
+
if c.Status(context.Background()).State != "incompatible" {
|
|
195
|
+
t.Fatal("incomplete approval treated as trusted disable")
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
func TestProcessRecordIsNotReadableByOrdinaryOSUsers(t *testing.T) {
|
|
200
|
+
c, _ := controlFixture(t)
|
|
201
|
+
s, err := os.Stat(filepath.Join(c.RunDir, "process.json"))
|
|
202
|
+
if err != nil || s.Mode().Perm() != 0600 {
|
|
203
|
+
t.Fatalf("process record permissions: %v %v", s, err)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
package helper
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"io"
|
|
8
|
+
"syscall"
|
|
9
|
+
"time"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// Also runs after exec: the parent can die between fork's prctl and exec.
|
|
13
|
+
func armParentDeath(expected int, set func() error, parent func() int) error {
|
|
14
|
+
if expected <= 1 || parent() != expected {
|
|
15
|
+
return errTrust
|
|
16
|
+
}
|
|
17
|
+
if err := set(); err != nil {
|
|
18
|
+
return err
|
|
19
|
+
}
|
|
20
|
+
if parent() != expected {
|
|
21
|
+
return errTrust
|
|
22
|
+
}
|
|
23
|
+
return nil
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type supervisedChild interface {
|
|
27
|
+
exited() (bool, error)
|
|
28
|
+
signal(syscall.Signal) error
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Caller is the sole reaper and signal owner. A zombie retains its PID until
|
|
32
|
+
// exited() reaps it; a background Wait goroutine would invalidate this proof.
|
|
33
|
+
func stopChild(ctx context.Context, child supervisedChild, grace time.Duration) error {
|
|
34
|
+
if err := ctx.Err(); err != nil {
|
|
35
|
+
return err
|
|
36
|
+
}
|
|
37
|
+
done, err := child.exited()
|
|
38
|
+
if err != nil || done {
|
|
39
|
+
return err
|
|
40
|
+
}
|
|
41
|
+
if err := child.signal(syscall.SIGTERM); err != nil {
|
|
42
|
+
return err
|
|
43
|
+
}
|
|
44
|
+
escalate := time.Now().Add(grace)
|
|
45
|
+
killed := false
|
|
46
|
+
for {
|
|
47
|
+
if err := ctx.Err(); err != nil {
|
|
48
|
+
return err
|
|
49
|
+
}
|
|
50
|
+
done, err := child.exited()
|
|
51
|
+
if err != nil || done {
|
|
52
|
+
return err
|
|
53
|
+
}
|
|
54
|
+
if !killed && !time.Now().Before(escalate) {
|
|
55
|
+
if err := child.signal(syscall.SIGKILL); err != nil {
|
|
56
|
+
return err
|
|
57
|
+
}
|
|
58
|
+
killed = true
|
|
59
|
+
}
|
|
60
|
+
timer := time.NewTimer(time.Millisecond)
|
|
61
|
+
select {
|
|
62
|
+
case <-ctx.Done():
|
|
63
|
+
timer.Stop()
|
|
64
|
+
return ctx.Err()
|
|
65
|
+
case <-timer.C:
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Read to actual connection EOF. A LimitReader's synthetic EOF at the size
|
|
71
|
+
// cap can never certify supervisor exit, so reaching that cap is a refusal.
|
|
72
|
+
func readSupervisorReply(reader io.Reader, want Inventory, nonce string) error {
|
|
73
|
+
b, err := io.ReadAll(io.LimitReader(reader, 4097))
|
|
74
|
+
if err != nil || len(b) > 4096 {
|
|
75
|
+
return errTrust
|
|
76
|
+
}
|
|
77
|
+
var res Response
|
|
78
|
+
d := json.NewDecoder(bytes.NewReader(b))
|
|
79
|
+
d.DisallowUnknownFields()
|
|
80
|
+
if d.Decode(&res) != nil || d.Decode(new(any)) != io.EOF || !res.OK || res.Error != "" || res.Inventory != want || res.Nonce != nonce {
|
|
81
|
+
return errTrust
|
|
82
|
+
}
|
|
83
|
+
return nil
|
|
84
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
package helper
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"strings"
|
|
8
|
+
"syscall"
|
|
9
|
+
"testing"
|
|
10
|
+
"time"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func TestParentDeathSetupChecksBeforeAndAfterPrctl(t *testing.T) {
|
|
14
|
+
for _, tc := range []struct {
|
|
15
|
+
before, after int
|
|
16
|
+
setErr error
|
|
17
|
+
ok bool
|
|
18
|
+
}{
|
|
19
|
+
{42, 42, nil, true}, {1, 1, nil, false}, {42, 1, nil, false}, {42, 42, syscall.EPERM, false},
|
|
20
|
+
} {
|
|
21
|
+
calls, sets := 0, 0
|
|
22
|
+
err := armParentDeath(42, func() error { sets++; return tc.setErr }, func() int {
|
|
23
|
+
calls++
|
|
24
|
+
if calls == 1 {
|
|
25
|
+
return tc.before
|
|
26
|
+
}
|
|
27
|
+
return tc.after
|
|
28
|
+
})
|
|
29
|
+
if (err == nil) != tc.ok {
|
|
30
|
+
t.Fatalf("%+v: %v", tc, err)
|
|
31
|
+
}
|
|
32
|
+
if tc.before != 42 && sets != 0 {
|
|
33
|
+
t.Fatal("already reparented child armed")
|
|
34
|
+
}
|
|
35
|
+
if tc.ok && calls != 2 {
|
|
36
|
+
t.Fatal("parent race not checked")
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type childFixture struct {
|
|
42
|
+
reaped bool
|
|
43
|
+
err error
|
|
44
|
+
signals []syscall.Signal
|
|
45
|
+
exitOn syscall.Signal
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func (c *childFixture) exited() (bool, error) { return c.reaped, c.err }
|
|
49
|
+
func (c *childFixture) signal(s syscall.Signal) error {
|
|
50
|
+
if c.reaped {
|
|
51
|
+
panic("signalled a reaped child")
|
|
52
|
+
}
|
|
53
|
+
c.signals = append(c.signals, s)
|
|
54
|
+
if s == c.exitOn {
|
|
55
|
+
c.reaped = true
|
|
56
|
+
}
|
|
57
|
+
return nil
|
|
58
|
+
}
|
|
59
|
+
func TestOnlyUnreapedOwnedChildCanBeSignalled(t *testing.T) {
|
|
60
|
+
for _, tc := range []struct {
|
|
61
|
+
reaped bool
|
|
62
|
+
err error
|
|
63
|
+
exit syscall.Signal
|
|
64
|
+
count int
|
|
65
|
+
ok bool
|
|
66
|
+
}{
|
|
67
|
+
{true, nil, 0, 0, true}, {false, syscall.ECHILD, 0, 0, false}, {false, nil, syscall.SIGTERM, 1, true}, {false, nil, syscall.SIGKILL, 2, true}, {false, nil, 0, 2, false},
|
|
68
|
+
} {
|
|
69
|
+
c := &childFixture{reaped: tc.reaped, err: tc.err, exitOn: tc.exit}
|
|
70
|
+
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
|
|
71
|
+
err := stopChild(ctx, c, time.Millisecond)
|
|
72
|
+
cancel()
|
|
73
|
+
if (err == nil) != tc.ok || len(c.signals) != tc.count {
|
|
74
|
+
t.Fatalf("%+v: err=%v signals=%v", tc, err, c.signals)
|
|
75
|
+
}
|
|
76
|
+
if c.reaped {
|
|
77
|
+
n := len(c.signals)
|
|
78
|
+
if stopChild(context.Background(), c, time.Millisecond) != nil || len(c.signals) != n {
|
|
79
|
+
t.Fatal("reaped PID reused")
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
func TestCancelledStopCannotSignal(t *testing.T) {
|
|
85
|
+
ctx, cancel := context.WithCancel(context.Background())
|
|
86
|
+
cancel()
|
|
87
|
+
c := &childFixture{}
|
|
88
|
+
if stopChild(ctx, c, time.Millisecond) == nil || len(c.signals) != 0 {
|
|
89
|
+
t.Fatal("cancelled operation signalled")
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func TestSupervisorReplyRequiresRealEOFAndStrictBoundedAcknowledgement(t *testing.T) {
|
|
94
|
+
i := fixture(t).Expected
|
|
95
|
+
nonce := strings.Repeat("c", 64)
|
|
96
|
+
good, _ := json.Marshal(Response{Inventory: i, Nonce: nonce, OK: true})
|
|
97
|
+
for _, tc := range []struct {
|
|
98
|
+
data []byte
|
|
99
|
+
ok bool
|
|
100
|
+
}{
|
|
101
|
+
{good, true}, {append(append([]byte{}, good...), []byte(" {}")...), false},
|
|
102
|
+
{bytes.Replace(good, []byte(nonce), []byte(strings.Repeat("d", 64)), 1), false},
|
|
103
|
+
{append(append([]byte{}, good[:len(good)-1]...), []byte(",\"unknown\":true}")...), false},
|
|
104
|
+
{append(append([]byte{}, good...), bytes.Repeat([]byte(" "), 4097)...), false},
|
|
105
|
+
} {
|
|
106
|
+
if (readSupervisorReply(bytes.NewReader(tc.data), i, nonce) == nil) != tc.ok {
|
|
107
|
+
t.Fatalf("reply accepted=%v expected=%v", !tc.ok, tc.ok)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if readSupervisorReply(&brokenReplyReader{data: good}, i, nonce) == nil {
|
|
111
|
+
t.Fatal("read error treated as cooperative exit")
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type brokenReplyReader struct{ data []byte }
|
|
116
|
+
|
|
117
|
+
func (r *brokenReplyReader) Read(p []byte) (int, error) {
|
|
118
|
+
if len(r.data) == 0 {
|
|
119
|
+
return 0, syscall.ECONNRESET
|
|
120
|
+
}
|
|
121
|
+
n := copy(p, r.data)
|
|
122
|
+
r.data = r.data[n:]
|
|
123
|
+
return n, nil
|
|
124
|
+
}
|