feat(up): support cap_add, cap_drop, tmpfs, shm_size, init and ulimits - #152
Open
Mikimoto wants to merge 6 commits into
Open
feat(up): support cap_add, cap_drop, tmpfs, shm_size, init and ulimits#152Mikimoto wants to merge 6 commits into
Mikimoto wants to merge 6 commits into
Conversation
…laim
Three problems found by a fresh-context review of this branch, all verified
against the installed apple/container sources rather than inferred:
ulimits: the decoder tried [String: String] then [String: Int] and fell back to
nil. Compose also allows a {soft, hard} pair, which matched neither, so a file
using the long form silently lost its whole ulimits map including any sibling
entries in short form. container run --ulimit takes <type>=<soft>[:<hard>]
(Parser.rlimit) and its type names match Compose's exactly, so the long form is
directly expressible. Both forms now normalise through UlimitValue, and an entry
that cannot be read throws instead of nilling the map.
tmpfs: same silent-nil shape for a value that is neither a list nor a string;
now throws. Options are also trimmed, so "/run:noexec, mode=0755" no longer
drops mode by failing its prefix test.
Capabilities: the comment claimed cap_drop was emitted before cap_add "so that
cap_drop: [ALL] followed by a narrow cap_add behaves as Compose specifies".
That is false. container collects the two flags into separate arrays
(Flags.swift) and computes the effective set in RuntimeService.effectiveCapabilities
- drop-ALL clears the base, adds are applied, individual drops removed - so the
command-line order carries no meaning. Two tests asserted that ordering; they
now assert that every declared capability reaches its flag, which is the real
invariant.
The six new keys also went through the run-args builder without variable
interpolation while every neighbouring key resolved ${VAR}; they now take the
environment and resolve it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat(up): support cap_add, cap_drop, tmpfs, shm_size, init and ulimits
Summary
Six container-hardening compose keys are currently absent from
Service'sCodingKeys. Swift'sCodableignores unknown keys, so today they are dropped with no warning: a compose file thatdeclares
cap_drop: [ALL]and a read-only rootfs withtmpfsmounts runs with the defaultcapability set and no tmpfs, and nothing in the output says so.
All six have a
container runequivalent. This adds them, plus reporting for two things that donot.
container runcap_add/cap_drop--cap-add/--cap-droptmpfs--mount type=tmpfs,target=…shm_size--shm-sizeinit--initulimits--ulimit <type>=<soft>[:<hard>]network_modeWhy the mapping is a pure function
The mapping lives in
ComposeUp.hardeningRunArgs(for:environment:)rather than inline in theargument builder, and the tests call it directly.
The reason is concrete. The existing tests cover Codable parsing and the already-extracted pure
helpers (
clampMemoryLimit,composePortToRunArg,networkRunArg), butrunCommandArgshas notest seam:
grep -rn runCommandArgsreturns 33 hits inComposeUp.swiftand 0 inTests/.That gap is why
healthcheck.timeoutcan be decoded (Healthcheck.swift:48,61,79), asserted on bytwo parsing tests, and still never reach
waitUntilServiceIsHealthy— no test can see thedifference. Adding six more keys the same way would have reproduced it six more times.
Measured against
container1.0.0, not inferredTwo findings shaped the implementation. Both are reproducible on macOS 27.0 /
container1.0.0.--tmpfssilently mounts at the literal path when given Compose-style options.There is no error.
/runis not mounted, and a directory named/run:noexec,nosuidexistsinstead. A read-only container then fails to write
/runwith a permission error that pointsnowhere near the cause. This change therefore uses
--mount type=tmpfsexclusively.--mount type=tmpfsaccepts onlytarget,modeandsize.So
noexec,nosuid,nodev,uidandgidcannot be expressed. They are dropped — butreported, never silently:
That second warning is not hypothetical. The mount is root-owned, so:
A service running as a non-root user with a read-only rootfs — PostgreSQL putting its socket in
/run/postgresqlis the usual case — will fail to start, and the reason is worth one line ofoutput.
network_modecontainer runhas no way to express "no network": a container started without--networkstilljoins the default network and gets an address.
network_modeis therefore parsed only so it can bereported, and produces no run arguments.
Capability ordering is deliberately not asserted
--cap-addand--cap-dropare collected into two separate arrays (Flags.swift:231-241) and theeffective set is computed in
RuntimeService.effectiveCapabilities—cap_drop: ALLclears thebase, adds are applied, then individual drops are removed. The order the flags appear in on the
command line carries no meaning, so the tests assert that every declared capability reaches its
flag rather than asserting a sequence.
ulimitsCompose allows both
nofile: 65535andnofile: {soft: 20000, hard: 40000}.container runtakes<type>=<soft>[:<hard>](Parser.rlimit) and its type names match Compose's exactly, so both formsmap directly. They are normalised through a small
UlimitValuedecoder.An entry that cannot be read throws rather than nilling the map. Nilling would drop the sibling
entries with it, which is the same silent-loss shape this change set exists to remove.
tmpfsdoesthe same for a value that is neither a list nor a string.
Tests
Two suites:
HardeningArgsTests— per-key parsing and flag mapping, including theulimitslong form,whitespace after a comma in tmpfs options,
${VAR}interpolation, and the two throwing paths.HardeningComposeIntegrationTests— the same keys through a whole compose document that sharesthem via a YAML anchor and merge keys. A parser that failed to resolve
<<:would pass everyper-key test and fail here.
Verification on macOS 27.0 / Swift 6.4, branched from
main@6e6aaf0:swift buildswift test(static suites)mainis 236 in 21)git diff main..HEAD --checkEvery new behaviour was mutation-checked: reverting it turns the covering test red. The capability
ordering was the one case where a test stayed green under mutation for a good reason, which is what
led to dropping that assertion.
Notes and limits
container execon the host does not guarantee the guest process is reaped; notintroduced here, but relevant to anything built on these flags.
size=is passed through as written.containerinterprets it in MiB, so a byte-valuedsize=1000000truncates to 0. Compose's own units are not translated; left as-is to avoidguessing at intent.
security_optandloggingremain unsupported:container inspect's configuration schema hasno field for the former, and there is no log-driver concept for the latter.
stop_grace_periodis deliberately untouched — feat: add/support stop_grace_period #150 is already open for it.