Repository navigation
Conversation
31326ad to
d658210
Compare
|
@kolyshkin, this fixes parsing in |
|
So I think we should use the same validation rules as the kernel (i.e. writing to The kernel reads
After parsing, the values must satisfy:
1. The kernel accepts but
|
cpu.max |
Kernel result | Why crun rejects it |
|---|---|---|
"\tmax", "\nmax 100000" |
max / current period, max / 100000 | only ' ' is skipped before max (a number may start with any whitespace, because strtoll() skips it) |
"50000\n100000", "max\n100000" |
50000 / 100000, max / 100000 | a newline is not accepted as the separator |
"50000 100000 1", "50000 100000x", "50000 100000\n1" |
50000 / 100000 | trailing garbage |
"50000 -1", "50000 +100000", "50000 abc" |
50000 / current period (the invalid period is ignored) | invalid period |
"50000+200000", "50000-1", "50000x 100000" |
50000 / current period; 50000 / 100000 for the last one | garbage attached to the quota |
"18446744073709552616" (2^64 + 1000) |
quota 1000 (wraps around) | ERANGE |
2. parse_cpu_max() accepts but the kernel rejects
Both drivers fail. With systemd, the scope has already been set up when the
write to the container's cpu.max returns EINVAL, so the error shows up
later and is less clear.
cpu.max |
crun result | Why the kernel rejects it |
|---|---|---|
"+50000 100000" |
50000 / 100000 | %llu does not accept a + sign, and "+50000" is not "max" |
"50000 0", "max 0" |
period 0. With systemd this silently means "no period", and the default is used | period < 1000 |
"50000 999", "50000 1000001", "50000 18446744073709551615" |
period passed to systemd as is (UINT64_MAX is USEC_INFINITY) |
period out of [1000, 1000000] |
"999 100000", "17592186044416 100000" |
quota passed to systemd as is | quota out of [1000, 2^44−1] |
3. Both accept, but with different values
cpu.max |
Kernel | crun |
|---|---|---|
"000000000000000500001000" (a 24-character word) |
quota token "00000000000000050000" is cut at 20 characters, and the remaining "1000" becomes the period: 50000 / 1000 |
500001000 / default period |
"50000" (no period) |
keeps the cgroup's current period | linux.resources.cpu.period if set, otherwise the systemd default (100000) |
The last row only matters if the current period differs from what crun
sends to systemd, so in practice it probably does not.
Notes
- Group 1 is the only one where the systemd driver fails and the cgroupfs
driver works. Most of it is rejected on purpose by this PR, and runc
rejects the same values. Only"\tmax"looks like an accidental
inconsistency. - Group 2 is easiest to close with range checks in
parse_cpu_max()
itself.
|
To clarify: I think our rules must be at least as strict as the kernel (say the kernel does not accept |
d658210 to
0dbc50c
Compare
|
Thanks for going through this so carefully. The tables made it easy to see what was off. I agree with the rule: be at least as strict as the kernel, stricter is fine, but don't reject things like Group 1. Tabs and other whitespace now work around and between the two values, and so do empty lines before or after the value, so I kept rejecting a newline between the two values ( Group 2. I added the same checks the kernel does: no sign on the quota, a period between 1000 and 1000000, and a quota between 1000 and 2^44−1. These values now fail early with Group 3. The 24-character case is now rejected: a quota longer than 20 characters is an error. I took a closer look at I also wrote each test value to a real |
0dbc50c to
8275681
Compare
|
@kolyshkin while working on this, I looked for the same kind of problem in other parts of crun and found a few. I'm sharing them here so you know about them. I'll send fixes as separate PRs once this one is merged. 1. Other unified values with the systemd driver
2. The systemd driver rejects values that cgroupfs accepts This is allowed under the "stricter is fine" rule, but the two drivers behave differently:
I'm not sure these are worth supporting. What do you think? 3. The "memory limit could be too low" hint
|
|
@harche this looks much better now, thanks! Can you split the reading of the missing period during update into a separate commit? Also, Other than that, LGTM |
With the systemd driver, parse_cpu_max() turns cpu.max from the unified map into CPUQuotaPerSecUSec and CPUQuotaPeriodUSec, while the raw string is still written to the container's cpu.max. When crun and the kernel read the string differently, the scope and the container end up with different limits, or the container fails to start with an sd-bus error. Values the kernel reads but crun rejected: - "\tmax", "\nmax 100000": only spaces were allowed before the quota. Accept any whitespace, and empty lines before and after the value. A newline between the quota and the period is still rejected: write_unified_resources() writes each line separately, so the kernel sees "50000\n100000" as two writes, "50000" and then "100000". Values crun accepted but the kernel rejects, which made the error show up only after the scope was set up: - "max5": no separator was required after "max". - "+50000 100000": the kernel does not accept a sign on the quota. - "50000 0", "50000 1000001", "999 100000", "17592186044416 100000": the period must be in [1000, 1000000], and the quota in [1000, 2^44 - 1]. A period of 0 even meant "no period" to systemd. Values both accept but read differently: - "50000+200000", "50000 -1": crun read a period after the sign, and strtoull() wrapped the negative one around, while the kernel ignores what follows the quota's digits and an invalid period, and keeps the current period. Reject them, rather than accept such values too: require a number without a sign, separated by whitespace, and allow only whitespace after the values. - "000000000000000500001000": the kernel reads at most 20 characters for the quota and uses the rest as the period. Reject a longer quota. Closes: containers#2405 Signed-off-by: Harshal Patil <12152047+harche@users.noreply.github.com> Assisted-by: Claude Code (claude-opus-5-5)
Move the reading of a cgroup's cpu.max out of complete_cpu_max(), so that it can be used by the systemd driver too. Signed-off-by: Harshal Patil <12152047+harche@users.noreply.github.com> Assisted-by: Claude Code (claude-opus-5-5)
When cpu.max in the unified map has no period, the kernel keeps the cgroup's current period, but on update crun computed CPUQuotaPerSecUSec with the default period. For a container with a period of 200000, updating cpu.max to "50000" gave the container "50000 200000", while systemd recorded CPUQuotaPerSecUSec=500ms instead of 250ms. Read the current period from the container's cpu.max instead. Signed-off-by: Harshal Patil <12152047+harche@users.noreply.github.com> Assisted-by: Claude Code (claude-opus-5-5)
8275681 to
14f7727
Compare
read_cpu_max() treated any error reading cpu.max as "not available". With the systemd driver, when cpu.max could not be read for another reason, such as EIO, crun update computed CPUQuotaPerSecUSec with the default period and sent it to systemd. The update then failed when writing the container's cpu.max, but the scope kept the new limit. For a container at "100000 200000", updating the quota to 30000 left the scope at "60000 200000" and the container at "100000 200000". Ignore only ENOENT, which is what happens when the cpu controller is not enabled, and report other errors, so the update fails before anything is changed. Signed-off-by: Harshal Patil <12152047+harche@users.noreply.github.com> Assisted-by: Claude Code (claude-opus-5-5)
14f7727 to
ba90afb
Compare
|
@harche can you use your real email in |
|
@kolyshkin that's the address I use to sign off everywhere, including my Kubernetes commits. It's my GitHub noreply address, and I use it to keep my email away from spammers, since commit history is public and gets scraped. It's tied to my GitHub account, the name is my real name, and the DCO check on this PR passes. |
parse_cpu_max()read somecpu.maxvalues differently from the kernel. With the systemd driver, this could give the scope and the container different CPU limits, or make the container fail to start with an sd-bus error. Details are in #2405.This makes the parser follow the kernel's rules, and stay stricter where that makes sense:
write_unified_resources()writes each line separately, so the kernel would see"50000\n100000"as two writes.maxfor the quota), and only whitespace may follow them.crun update, whencpu.maxhas no period, the current period is read from the container'scpu.max, like the kernel does, instead of using the default to computeCPUQuotaPerSecUSec.Well-formed values are read as before, including what the kernel writes in
cpu.max(max 100000\n), whichcomplete_cpu_max()reads back.Testing
tests/tests_libcrun_cgroup.c:test_parse_cpu_maxfor valid values (tabs, leading and trailing newlines, range limits),test_parse_cpu_max_invalidfor the malformed and out-of-range ones.cpu.max(Fedora 44, kernel 6.19): whateverparse_cpu_max()accepts, the kernel accepts with the same values, and every out-of-range value is rejected by both.unified["cpu.max"]set to"50000\n100000"or"max\n100000"is rejected withinvalid value for cpu.max(with the cgroupfs driver, the container ends up with quota 100000)."\nmax 100000"and"50000 100000\n\n"give the scope and the container the same limits.cpu.period200000, then rancrun updatewithunified["cpu.max"] = "50000". Before, the container got50000 200000but systemd recordedCPUQuotaPerSecUSec=500ms. Now systemd records250mswith a200msperiod, which matches.tests/test_update.pypasses with bothCGROUP_MANAGER=cgroupfsandsystemd.tests/test_cgroup_resources.pypasses exceptcgroup-resources-hugetlb-limits, which fails the same way onmain(no hugetlb controller in the VM).make clang-formatis clean.