From 510519727082428ed08262650927272dc14c36b1 Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Sat, 31 Oct 2026 01:35:06 +0800 Subject: [PATCH] E2.3.2: Apply and verify the mapped policy Mapped creation must carry the exact compiled kernel policy through preparation, inhibited startup and trusted pre-release observation. Add one typed overlay over the existing strict ordinary parser, require independently supplied startup capability equality, and bind the full ABI/capability/policy tuple to the existing sandbox proof. Transmit and verify explicit no-removal/no-restart policy while preserving ordinary request serialization. Keep public mapped execution inhibited and leave production F observations, effective runtime qualification and final entry-plan construction behind their existing gates. Retain the C1 diagnostic with a real production waiter and a host regression over its generated binding, create stdin and causal EOF. Validation: fresh independent full review; Python packaging and CI-tool tests; targeted Go vet; all shipping/conformance Go packages; linux-amd64 build and CLI smoke. Retained WSL ordinary-tmpfs exclusion and explicit Docker skip apply; live runtime and exact-head PR CI remain separate gates. --- internal/dockerdeploy/mapped_sandbox_proof.go | 15 +- .../dockerdeploy/podman_application_create.go | 1 + internal/dockerdeploy/podman_mapped_policy.go | 157 +++++ .../dockerdeploy/podman_mapped_policy_test.go | 590 ++++++++++++++++++ internal/dockerdeploy/podman_mapped_waiter.go | 83 ++- .../dockerdeploy/podman_mapped_waiter_test.go | 21 +- .../dockerdeploy/podman_mapping_create.go | 19 +- .../podman_mapping_integration_test.go | 156 ++++- internal/probe/mapped_exec.go | 13 + 9 files changed, 1021 insertions(+), 34 deletions(-) create mode 100644 internal/dockerdeploy/podman_mapped_policy.go create mode 100644 internal/dockerdeploy/podman_mapped_policy_test.go diff --git a/internal/dockerdeploy/mapped_sandbox_proof.go b/internal/dockerdeploy/mapped_sandbox_proof.go index c61cd7eb..13e96550 100644 --- a/internal/dockerdeploy/mapped_sandbox_proof.go +++ b/internal/dockerdeploy/mapped_sandbox_proof.go @@ -30,6 +30,8 @@ type mappedSandboxRequestV1 struct { RuntimePolicy canonical.Digest `json:"runtime_policy"` NetworkPolicy canonical.Digest `json:"network_policy"` RequiredCode []string `json:"required_code"` + KernelPolicy canonical.Digest `json:"kernel_policy"` + KernelFacts mappedKernelFactsV1 `json:"kernel_facts"` } type mappedCodePathV1 struct { @@ -44,6 +46,8 @@ type mappedSandboxProofV1 struct { Request canonical.Digest NetworkPolicy canonical.Digest ReadOnlyRoot bool + KernelPolicy canonical.Digest + KernelFacts mappedKernelFactsV1 // Code is F's complete closure from the pinned image, including loaders, // interpreters and dependency search paths. ObservedCode is its live view. Code, ObservedCode []mappedCodePathV1 @@ -66,7 +70,8 @@ func (a *ownedMappedWaiterV1) sandboxRequest() (mappedSandboxRequestV1, error) { lock.FinalImage.RootFSSubject != p.state.Current.RootFSSubject { return mappedSandboxRequestV1{}, fmt.Errorf("mapped sandbox generation or image changed") } - body, _, err := podmanApplicationCreateRequest(p.spec, p.namespace) + create, err := p.request() + body := create.body separator := slices.Index(body.Cmd, "--") if err != nil || separator < 0 || separator+1 >= len(body.Cmd) || !mappedCodeAbsoluteV1(body.Cmd[separator+1]) { return mappedSandboxRequestV1{}, fmt.Errorf("mapped sandbox executable is unavailable") @@ -78,7 +83,9 @@ func (a *ownedMappedWaiterV1) sandboxRequest() (mappedSandboxRequestV1, error) { slices.Sort(required) // ReadBuildLock already validated the closed string-only network policy. network, _ := canonical.Sum("mapped-network", "v1", lock.RuntimePolicy.Network) - return mappedSandboxRequestV1{a.binding, a.container, strconv.Itoa(a.pid), policy, network, slices.Compact(required)}, nil + return mappedSandboxRequestV1{Binding: a.binding, Container: a.container, PID: strconv.Itoa(a.pid), + RuntimePolicy: policy, NetworkPolicy: network, RequiredCode: slices.Compact(required), + KernelPolicy: canonical.Digest(p.policy.identity), KernelFacts: p.policy.facts()}, nil } func verifyMappedSandboxV1(ctx context.Context, state deploy.StateV1, evidence deploy.IdentityMappingEvidenceV1, request mappedSandboxRequestV1) error { @@ -86,6 +93,9 @@ func verifyMappedSandboxV1(ctx context.Context, state deploy.StateV1, evidence d if !ok || ctx.Err() != nil { return fmt.Errorf("mapped sandbox observation provider is unavailable") } + if err := request.KernelFacts.validate(); err != nil || request.KernelPolicy != request.KernelFacts.Identity { + return fmt.Errorf("mapped sandbox requires the exact compiled kernel policy tuple") + } digest, err := canonical.Sum("mapped-sandbox", "v1", request) if err != nil { return err @@ -97,6 +107,7 @@ func verifyMappedSandboxV1(ctx context.Context, state deploy.StateV1, evidence d return fmt.Errorf("mapped sandbox observation failed: %w", err) } if ctx.Err() != nil || proof.Request != digest || proof.NetworkPolicy != request.NetworkPolicy || !proof.ReadOnlyRoot || + proof.KernelPolicy != request.KernelPolicy || proof.KernelFacts != request.KernelFacts || len(proof.Code) == 0 || !slices.Equal(proof.Code, proof.ObservedCode) || proof.Mounts == nil { return fmt.Errorf("mapped sandbox proof is missing, stale or substituted") } diff --git a/internal/dockerdeploy/podman_application_create.go b/internal/dockerdeploy/podman_application_create.go index b6bf3674..ef9b006f 100644 --- a/internal/dockerdeploy/podman_application_create.go +++ b/internal/dockerdeploy/podman_application_create.go @@ -36,6 +36,7 @@ type podmanApplicationCreate struct { CapDrop, CapAdd, SecurityOpt, Dns []string Tmpfs map[string]string Mounts []podmanApplicationMount + RestartPolicy *mappedRestartPolicyV1 `json:",omitempty"` } } diff --git a/internal/dockerdeploy/podman_mapped_policy.go b/internal/dockerdeploy/podman_mapped_policy.go new file mode 100644 index 00000000..ecb8f7ad --- /dev/null +++ b/internal/dockerdeploy/podman_mapped_policy.go @@ -0,0 +1,157 @@ +package dockerdeploy + +import ( + "fmt" + "slices" + "strconv" + "strings" + "time" + + "github.com/omry/reploy/internal/canonical" + "github.com/omry/reploy/internal/mappedlaunch" + "github.com/omry/reploy/internal/probe" +) + +type mappedRestartPolicyV1 struct { + Name string + MaximumRetryCount int +} + +// These names are in Linux capability-number order, matching the compiler's +// closed v6.6 catalog. Setup authority never becomes an application grant. +var mappedCapabilityNamesV1 = strings.Fields(`CHOWN DAC_OVERRIDE DAC_READ_SEARCH FOWNER FSETID KILL +SETGID SETUID SETPCAP LINUX_IMMUTABLE NET_BIND_SERVICE NET_BROADCAST NET_ADMIN NET_RAW +IPC_LOCK IPC_OWNER SYS_MODULE SYS_RAWIO SYS_CHROOT SYS_PTRACE SYS_PACCT SYS_ADMIN +SYS_BOOT SYS_NICE SYS_RESOURCE SYS_TIME SYS_TTY_CONFIG MKNOD LEASE AUDIT_WRITE +AUDIT_CONTROL SETFCAP MAC_OVERRIDE MAC_ADMIN SYSLOG WAKE_ALARM BLOCK_SUSPEND +AUDIT_READ PERFMON BPF CHECKPOINT_RESTORE`) + +// Only this typed overlay can carry mapped compiler output into create. The +// ordinary parser and its capability/profile allowlist stay unchanged. +type mappedPodmanCreateRequestV1 struct { + body podmanApplicationCreate + name string + policy mappedSeccompPolicyV1 +} + +func mappedPodmanApplicationCreateRequestV1(spec CommandSpec, namespace string, policy mappedSeccompPolicyV1) (mappedPodmanCreateRequestV1, error) { + var result mappedPodmanCreateRequestV1 + compiled, err := compileMappedSeccompV1(podmanDockerSeccompProfile, policy.abi, policy.finalCapabilities, policy.setupCapabilities, policy.kernelLastCapability) + if err != nil || policy != compiled { + return result, fmt.Errorf("mapped create requires an exact compiled kernel policy") + } + body, name, err := podmanApplicationCreateRequest(spec, namespace) + if err != nil { + return result, err + } + if body.Tty { + return result, fmt.Errorf("mapped create requires qualified non-TTY sandbox stdin transport") + } + if err := validateMappedCreateStartupV1(body.Cmd, policy); err != nil { + return result, err + } + setup := policy.finalCapabilities + for _, name := range body.HostConfig.CapAdd { + index := slices.Index(mappedCapabilityNamesV1, name) + if index < 0 { + return result, fmt.Errorf("mapped create has an unknown setup capability") + } + setup |= uint64(1) << index + } + if setup != policy.setupCapabilities { + return result, fmt.Errorf("mapped setup capabilities differ from the exact required union") + } + body.HostConfig.CapAdd = []string{} + for bit, name := range mappedCapabilityNamesV1 { + if setup&(uint64(1)< 0 && strings.HasPrefix(args[0], mappedCapabilityArgumentV1) {