|
| 1 | +//go:build linux |
| 2 | + |
| 3 | +/* |
| 4 | + Copyright The containerd Authors. |
| 5 | +
|
| 6 | + Licensed under the Apache License, Version 2.0 (the "License"); |
| 7 | + you may not use this file except in compliance with the License. |
| 8 | + You may obtain a copy of the License at |
| 9 | +
|
| 10 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | +
|
| 12 | + Unless required by applicable law or agreed to in writing, software |
| 13 | + distributed under the License is distributed on an "AS IS" BASIS, |
| 14 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | + See the License for the specific language governing permissions and |
| 16 | + limitations under the License. |
| 17 | +*/ |
| 18 | + |
| 19 | +package shimtest |
| 20 | + |
| 21 | +import ( |
| 22 | + "strings" |
| 23 | + "testing" |
| 24 | + "time" |
| 25 | + |
| 26 | + taskAPI "github.com/containerd/containerd/api/runtime/task/v3" |
| 27 | + specs "github.com/opencontainers/runtime-spec/specs-go" |
| 28 | +) |
| 29 | + |
| 30 | +// requireSandboxHostnameCapability probes, independently of any |
| 31 | +// namespace-sharing behavior, whether the shim honors a member |
| 32 | +// container's request for CAP_SYS_ADMIN by running "hostname <name>" |
| 33 | +// once in an otherwise-unshared container. If the set fails, the |
| 34 | +// calling test is skipped rather than failed: granting a container's |
| 35 | +// requested Linux capabilities is a separate contract from namespace |
| 36 | +// sharing (which is what UTS tests in this file actually check), and a |
| 37 | +// shim that doesn't support capability requests at all shouldn't be |
| 38 | +// penalized on a contract it was never asked to implement here. |
| 39 | +func requireSandboxHostnameCapability(t *testing.T, env *sandboxEnv) { |
| 40 | + t.Helper() |
| 41 | + |
| 42 | + cid := createContainerInSandbox(t, env, []string{"/bin/hostname", "cap-probe-" + randomSuffix()}, |
| 43 | + withSandboxCtrOCIOpts(withCapabilities("CAP_SYS_ADMIN"))) |
| 44 | + waitResp, err := env.tc.Wait(env.ctx, &taskAPI.WaitRequest{ID: cid}) |
| 45 | + if err != nil { |
| 46 | + t.Fatalf("Task.Wait capability probe: %v", err) |
| 47 | + } |
| 48 | + env.tc.Delete(env.ctx, &taskAPI.DeleteRequest{ID: cid}) //nolint:errcheck |
| 49 | + if waitResp.GetExitStatus() != 0 { |
| 50 | + t.Skipf("shim did not grant the requested CAP_SYS_ADMIN to the container (hostname set failed); cannot verify UTS namespace behavior") |
| 51 | + } |
| 52 | +} |
| 53 | + |
| 54 | +// setSandboxHostname creates a member container that sets the UTS |
| 55 | +// namespace hostname via the standard "hostname <name>" command, |
| 56 | +// applying opts (e.g. withSandboxCtrNamespace(UTSNamespace, ...) to |
| 57 | +// join a shared UTS namespace) to its spec, and waits for it to exit. |
| 58 | +// |
| 59 | +// Callers must call requireSandboxHostnameCapability first: with the |
| 60 | +// capability precondition already verified separately, a non-zero exit |
| 61 | +// here is treated as a hard test failure rather than a skip. |
| 62 | +// |
| 63 | +// The standard "hostname <name>" exits immediately and silently on a |
| 64 | +// successful set rather than holding the namespace open itself (see |
| 65 | +// cmdHostname), so a caller that later observes the change is also |
| 66 | +// proving the namespace — and its hostname — outlives the process that |
| 67 | +// set it, not merely that a still-running setter's own namespace is |
| 68 | +// visible. |
| 69 | +func setSandboxHostname(t *testing.T, env *sandboxEnv, hostname string, opts ...func(*sandboxCtrSpec)) { |
| 70 | + t.Helper() |
| 71 | + |
| 72 | + opts = append(opts, withSandboxCtrOCIOpts(withCapabilities("CAP_SYS_ADMIN"))) |
| 73 | + cid := createContainerInSandbox(t, env, []string{"/bin/hostname", hostname}, opts...) |
| 74 | + |
| 75 | + waitResp, err := env.tc.Wait(env.ctx, &taskAPI.WaitRequest{ID: cid}) |
| 76 | + if err != nil { |
| 77 | + t.Fatalf("Task.Wait hostname setter: %v", err) |
| 78 | + } |
| 79 | + if waitResp.GetExitStatus() != 0 { |
| 80 | + t.Fatalf("hostname setter exit status: got %d, want 0 (CAP_SYS_ADMIN already verified available)", waitResp.GetExitStatus()) |
| 81 | + } |
| 82 | + env.tc.Delete(env.ctx, &taskAPI.DeleteRequest{ID: cid}) //nolint:errcheck |
| 83 | +} |
| 84 | + |
| 85 | +// readSandboxHostname creates a member container that prints its UTS |
| 86 | +// namespace hostname via the standard, argument-less "hostname" |
| 87 | +// command, applying opts to its spec, waits for it to exit, and |
| 88 | +// returns the trimmed hostname it reported. |
| 89 | +func readSandboxHostname(t *testing.T, env *sandboxEnv, opts ...func(*sandboxCtrSpec)) string { |
| 90 | + t.Helper() |
| 91 | + |
| 92 | + cid := createContainerInSandbox(t, env, []string{"/bin/hostname"}, opts...) |
| 93 | + waitResp, err := env.tc.Wait(env.ctx, &taskAPI.WaitRequest{ID: cid}) |
| 94 | + if err != nil { |
| 95 | + t.Fatalf("Task.Wait hostname reader: %v", err) |
| 96 | + } |
| 97 | + if waitResp.GetExitStatus() != 0 { |
| 98 | + t.Fatalf("hostname reader exit status: got %d, want 0", waitResp.GetExitStatus()) |
| 99 | + } |
| 100 | + // Allow a moment for the last of stdout to drain after exit (see |
| 101 | + // containerOutputSnapshot). |
| 102 | + time.Sleep(50 * time.Millisecond) |
| 103 | + out := strings.TrimSpace(containerOutputSnapshot(t, env, cid)) |
| 104 | + env.tc.Delete(env.ctx, &taskAPI.DeleteRequest{ID: cid}) //nolint:errcheck |
| 105 | + return out |
| 106 | +} |
| 107 | + |
| 108 | +// testMemberContainersShareUTS verifies that member containers of the |
| 109 | +// same sandbox can share a UTS namespace: a hostname change made by one |
| 110 | +// member container via the standard "hostname <name>" command is |
| 111 | +// visible — via the kernel's reported hostname, not a file or |
| 112 | +// environment variable — to a second, independently created member |
| 113 | +// container, even after the container that made the change has exited. |
| 114 | +// |
| 115 | +// The API contract: when a member container's OCI spec carries a host |
| 116 | +// path on its UTS namespace entry (e.g. this is how a caller expresses |
| 117 | +// Kubernetes' default of sharing one hostname across a pod's |
| 118 | +// containers), the shim must place that container in a UTS namespace |
| 119 | +// shared with its sandbox peers rather than a fresh, isolated one, and |
| 120 | +// that shared namespace must be owned by the sandbox rather than tied |
| 121 | +// to the lifetime of whichever container last changed its hostname. |
| 122 | +// This test only observes the externally visible result and does not |
| 123 | +// assume any particular mechanism a shim uses to provide it. It |
| 124 | +// intentionally uses a placeholder host path (see |
| 125 | +// withSandboxCtrNamespace) since only a live host has an actual |
| 126 | +// sandbox PID to put there. |
| 127 | +func (s *SandboxSuite) testMemberContainersShareUTS(t *testing.T) { |
| 128 | + sandboxID := containerID(t) |
| 129 | + env := startSandboxShim(t, s.cfg, sandboxID) |
| 130 | + |
| 131 | + requireSandboxHostnameCapability(t, env) |
| 132 | + |
| 133 | + hostname := "shared-uts-" + randomSuffix() |
| 134 | + setSandboxHostname(t, env, hostname, withSandboxCtrNamespace(specs.UTSNamespace, "/proc/1/ns/uts")) |
| 135 | + |
| 136 | + got := readSandboxHostname(t, env, withSandboxCtrNamespace(specs.UTSNamespace, "/proc/1/ns/uts")) |
| 137 | + if got != hostname { |
| 138 | + t.Fatalf("reader hostname: got %q, want %q", got, hostname) |
| 139 | + } |
| 140 | + |
| 141 | + t.Log("member containers share a UTS namespace: hostname change outlived the container that set it and was visible to a peer") |
| 142 | +} |
| 143 | + |
| 144 | +// testMemberContainersUTSNotShared verifies the converse of |
| 145 | +// testMemberContainersShareUTS: a member container that does not |
| 146 | +// request UTS sharing must not observe a peer's hostname change, even |
| 147 | +// though both containers belong to the same sandbox. |
| 148 | +// |
| 149 | +// The API contract mirrors testMemberContainersSharePID's converse and |
| 150 | +// testMemberContainersDevShmNotSharedWithoutIPC: the shim must key UTS |
| 151 | +// namespace sharing off the container's own UTS-namespace-sharing |
| 152 | +// signal, not off simply being a member of the same sandbox. |
| 153 | +func (s *SandboxSuite) testMemberContainersUTSNotShared(t *testing.T) { |
| 154 | + sandboxID := containerID(t) |
| 155 | + env := startSandboxShim(t, s.cfg, sandboxID) |
| 156 | + |
| 157 | + requireSandboxHostnameCapability(t, env) |
| 158 | + |
| 159 | + hostname := "should-not-be-visible-" + randomSuffix() |
| 160 | + setSandboxHostname(t, env, hostname, withSandboxCtrNamespace(specs.UTSNamespace, "/proc/1/ns/uts")) |
| 161 | + |
| 162 | + // No withSandboxCtrNamespace(UTSNamespace, ...): this container does |
| 163 | + // not request UTS sharing, so it must get its own, unaffected UTS |
| 164 | + // namespace. |
| 165 | + got := readSandboxHostname(t, env) |
| 166 | + if got == hostname { |
| 167 | + t.Fatalf("reader saw writer's hostname %q despite neither container sharing UTS", hostname) |
| 168 | + } |
| 169 | + |
| 170 | + t.Log("member container not sharing UTS correctly did not observe a peer's hostname change") |
| 171 | +} |
0 commit comments