Compare commits
14
Commits
63850321af
...
b5d8617b03
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5d8617b03
|
||
|
|
4ed4b428ba | ||
|
|
aa5c0f0f77 | ||
|
|
a446e8ff2c | ||
|
|
c25226b820 | ||
|
|
9336b9ce23 | ||
|
|
a3335922ce | ||
|
|
17e3db12ca | ||
|
|
4754cd2db6 | ||
|
|
4ca7ac0275 | ||
|
|
0d1ae05a7f | ||
|
|
d6f96ac311 | ||
|
|
28a1975396 | ||
|
|
671206e266 |
@@ -134,18 +134,29 @@ jobs:
|
||||
]);
|
||||
|
||||
/* A README.md that still carries the AI review notice has not been
|
||||
* looked at by its author yet, so there is nothing to review. */
|
||||
const readme = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: "README.md",
|
||||
ref: pr.data.head.sha,
|
||||
});
|
||||
if (Buffer.from(readme.data.content, "base64").toString()
|
||||
.includes("Remove this line to confirm you've reviewed this PR before submitting.")) {
|
||||
console.log("README.md still carries the review notice, skipping.");
|
||||
core.setOutput("skip", "true");
|
||||
return;
|
||||
* looked at by its author yet, so there is nothing to review. An
|
||||
* explicit "@claude review" answers the notice, so only the automatic
|
||||
* triggers are skipped, and a check that cannot run reviews anyway. */
|
||||
if (context.eventName === "pull_request_target") {
|
||||
let notice = false;
|
||||
try {
|
||||
const readme = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: "README.md",
|
||||
ref: pr.data.head.sha,
|
||||
});
|
||||
notice = Buffer.from(readme.data.content, "base64").toString()
|
||||
.includes("Remove this line to confirm you've reviewed this PR before submitting.");
|
||||
} catch (e) {
|
||||
console.log(`Failed to read README.md, assuming it has been reviewed: ${e.message}`);
|
||||
}
|
||||
|
||||
if (notice) {
|
||||
console.log("README.md still carries the review notice, skipping.");
|
||||
core.setOutput("skip", "true");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* Find or create tracking comment. */
|
||||
@@ -207,6 +218,7 @@ jobs:
|
||||
fs.writeFileSync("pr-context.json", JSON.stringify(prContext, null, 2));
|
||||
|
||||
- name: Upload PR context
|
||||
if: steps.context.outputs.skip != 'true'
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
|
||||
with:
|
||||
name: pr-context.json
|
||||
@@ -354,7 +366,7 @@ jobs:
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "[[ $(date +%s) -lt ${REVIEW_DEADLINE:-0} ]] || { echo 'Time is up: stop using tools and return your findings now.' >&2; exit 2; }"
|
||||
"command": "[ \"$(date +%s)\" -lt \"${REVIEW_DEADLINE:-0}\" ] || { echo 'Time is up: stop using tools and return your findings now.' >&2; exit 2; }"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -408,10 +420,21 @@ jobs:
|
||||
-p "$prompt" >"$OUTPUT/$name.log") || status=$?
|
||||
jq -Rc 'fromjson? | select(.type == "result" and .subtype == "success") | .structured_output | select(. != null)' \
|
||||
"$OUTPUT/$name.log" | tail -n 1 >"$OUTPUT/$name.json" || :
|
||||
if [[ -s "$OUTPUT/$name.json" ]]; then
|
||||
# An agent whose every tool call failed still returns an empty findings
|
||||
# array, which is indistinguishable from a clean review. Require that it
|
||||
# got at least one tool result other than its own structured output.
|
||||
local worked
|
||||
worked="$(jq -Rn '
|
||||
[inputs | fromjson?] as $events
|
||||
| [$events[] | select(.type == "assistant") | .message.content[]?
|
||||
| select(.type == "tool_use" and .name == "StructuredOutput") | .id] as $ignore
|
||||
| [$events[] | select(.type == "user") | .message.content[]?
|
||||
| select(.type == "tool_result" and .is_error != true and ([.tool_use_id] - $ignore | length > 0))]
|
||||
| length' <"$OUTPUT/$name.log")"
|
||||
if [[ -s "$OUTPUT/$name.json" ]] && (( worked > 0 )); then
|
||||
echo "${name}: done (status ${status})"
|
||||
else
|
||||
echo "${name}: no result (status ${status})"
|
||||
echo "${name}: no result (status ${status}, ${worked} tool call(s) succeeded)"
|
||||
rm -f "$OUTPUT/$name.json"
|
||||
fi
|
||||
}
|
||||
@@ -746,12 +769,12 @@ jobs:
|
||||
}
|
||||
/* An unterminated fence would swallow everything after it, so cut
|
||||
* it off before anything else is measured or truncated. */
|
||||
const fences = [...c.body.matchAll(/^```/gm)];
|
||||
const fences = [...c.body.matchAll(/^ {0,3}`{3,}/gm)];
|
||||
if (fences.length % 2 === 1) {
|
||||
console.log(` Dropping unterminated code fence in comment on ${c.path}:${c.line}.`);
|
||||
c = { ...c, body: c.body.slice(0, fences.at(-1).index).trimEnd() || "(empty)" };
|
||||
}
|
||||
const prose = c.body.replace(/^```[\s\S]*?^```[^\n]*/gm, "");
|
||||
const prose = c.body.replace(/^ {0,3}`{3,}[\s\S]*?^ {0,3}`{3,}[^\n]*/gm, "");
|
||||
if (prose.length > MAX_PROSE_LENGTH) {
|
||||
console.log(` Truncating comment on ${c.path}:${c.line}: ${prose.length} characters of prose exceeds ${MAX_PROSE_LENGTH}.`);
|
||||
/* Cutting inside a fence would leave a suggestion missing code,
|
||||
|
||||
@@ -48,14 +48,33 @@ jobs:
|
||||
if: startsWith(github.event_name, 'pull_request') && github.event.action != 'closed' && !github.event.pull_request.draft
|
||||
with:
|
||||
script: |
|
||||
/* Never read the full label set and write it back: other workflows
|
||||
* label the same pull request concurrently and setLabels() would
|
||||
* clobber whatever they added in the meantime. Only ever add or
|
||||
* remove the individual labels this step owns. */
|
||||
const removeLabel = async (name) => {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: name,
|
||||
});
|
||||
} catch (error) {
|
||||
/* Somebody else removed it first. */
|
||||
if (error.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
response = await github.rest.issues.listLabelsOnIssue({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
});
|
||||
|
||||
original = new Set(response.data.map(l => l.name));
|
||||
labels = new Set(original);
|
||||
labels = new Set(response.data.map(l => l.name));
|
||||
|
||||
good_to_merge = new Set([
|
||||
"good-to-merge/waiting-for-ci 👍",
|
||||
@@ -85,24 +104,22 @@ jobs:
|
||||
}
|
||||
}
|
||||
`, { id: context.payload.pull_request.node_id });
|
||||
} else if (Array.from(labels).filter(l => good_to_merge.has(l)).length == 0) {
|
||||
labels.add("please-review");
|
||||
} else if (!Array.from(good_to_merge).some(l => labels.has(l)) && !labels.has("please-review")) {
|
||||
await github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ["please-review"],
|
||||
});
|
||||
}
|
||||
|
||||
for (const label of ["reviewed/needs-rework 🔨",
|
||||
"ci-fails/needs-rework 🔥",
|
||||
"ci-failure-appears-unrelated",
|
||||
"needs-rebase"]) {
|
||||
labels.delete(label);
|
||||
}
|
||||
|
||||
if (labels.size != original.size || Array.from(labels).some(l => !original.has(l))) {
|
||||
await github.rest.issues.setLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: Array.from(labels),
|
||||
});
|
||||
if (labels.has(label)) {
|
||||
await removeLabel(label);
|
||||
}
|
||||
}
|
||||
|
||||
- name: Add please-review label on command in issue comment
|
||||
@@ -128,8 +145,7 @@ jobs:
|
||||
repo: context.repo.repo,
|
||||
});
|
||||
|
||||
original = new Set(response.data.map(l => l.name));
|
||||
labels = new Set(original);
|
||||
labels = new Set(response.data.map(l => l.name));
|
||||
|
||||
for (const label of ["please-review",
|
||||
"reviewed/needs-rework 🔨",
|
||||
@@ -144,14 +160,21 @@ jobs:
|
||||
"dont-merge 💣",
|
||||
"squash-on-merge",
|
||||
"quick-review 🏃♂️"]) {
|
||||
labels.delete(label);
|
||||
}
|
||||
if (!labels.has(label)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (labels.size != original.size || Array.from(labels).some(l => !original.has(l))) {
|
||||
await github.rest.issues.setLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: Array.from(labels),
|
||||
});
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: label,
|
||||
});
|
||||
} catch (error) {
|
||||
/* Somebody else removed it first. */
|
||||
if (error.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# ANCIENT-MOD: running on old kernels (target: 4.19)
|
||||
|
||||
Upstream requires kernel ≥ 5.10 (see `README`, "REQUIREMENTS") and dropped the fallback code for
|
||||
older kernels. This branch restores or adds fallbacks so the system can at least boot on 4.19.
|
||||
Code touched by this effort is marked with `ANCIENT-MOD` comments where practical.
|
||||
|
||||
Legend: **revert** = reverse-applied an upstream commit (possibly adjusted), **new** = new code
|
||||
because upstream code was refactored since the fallback was removed.
|
||||
|
||||
## Boot-critical
|
||||
|
||||
| Area | Kernel feature | Change | Where |
|
||||
|---|---|---|---|
|
||||
| Mount point checks (`is_mount_point_at()`, `path_is_mount_point()`), used by early `mount_setup()` for `/proc`, `/sys`, `/dev` with `MNT_FATAL` | `STATX_ATTR_MOUNT_ROOT` (5.8) | **new**: `xstatx_full()` emulates the attribute when it is required but missing: compares the mount id (from `/proc/self/fdinfo`) of the inode with its parent's (`..` for directories, parent path for others); same inode as parent ⇒ root ⇒ mount point. Replaces upstream fallback removed in `a98a6eb95c`. | `src/basic/stat-util.c` (`xstatx_emulate()`) |
|
||||
| `chase()` (used everywhere), `path_get_mnt_id_at()`, `fds_inode_and_mount_same()`, `XSTATX_MNT_ID_BEST` | `STATX_MNT_ID` (5.8/5.10) | **new**: same emulation fills `stx_mnt_id` from `/proc/self/fdinfo/<fd>` `mnt_id:` (available since 3.15). Replaces removals in `7ecfa87c56`, `e5a3127339`. | `src/basic/stat-util.c` |
|
||||
| cgroup2 mount | `memory_recursiveprot` (5.7) | **revert** (adjusted) of `80c5b63db5`: option is probed again instead of always passed; if it cannot be probed because `fsopen()` is missing (< 5.2) it is not used. | `src/shared/mount-setup.c` |
|
||||
| `mount_option_supported()` | `fsopen()` (5.2) | **revert** of part of `c2198d0c3f`: return `-EAGAIN` ("don't know") when the new mount API is unavailable. | `src/basic/mountpoint-util.c` |
|
||||
| `close_all_fds*()` (every fork/exec, `systemd-executor`) | `close_range()` (5.9) | **revert** of `c471cca093` and `d58c826c54`: fall back to iterating `/proc/self/fd` (or the frugal loop). | `src/basic/fd-util.c`, `src/basic/fd-util.h`, `src/test/test-fd-util.c` |
|
||||
| `pidref_set_pid()` (every process PID1 tracks, `safe_fork()`, `posix_spawn_wrapper()`) | `pidfd_open()` (5.3) | **revert** (adjusted) of part of `6e14c46bac`: ENOSYS ⇒ PidRef without fd (plain PID). | `src/basic/pidref.c` |
|
||||
| `pidref_wait_for_terminate_full()` with timeout | pidfd (5.3) | **new**: without pidfd poll `waitid(P_PID, …, WNOHANG)` every 10 ms instead of returning `-ENOMEDIUM`. | `src/basic/pidref.c` |
|
||||
| `pidfd_check_pidfs()`, `pidfd_info_mask_is_supported()`, `pidfd_get_inode_id_self_cached()` | pidfd (5.3) | **revert**/new: ENOSYS ⇒ "not supported" instead of hard error. | `src/basic/pidfd-util.c` |
|
||||
| sd-event child sources (`sd_event_add_child()`, used by udevd, logind, …) | pidfd (5.3), `waitid(P_PIDFD)` (5.4) | **new** (upstream removed the non-pidfd path in `6e14c46bac` and later made the code pidfd-only): if `pidfd_open()` is unsupported the source works on the PID via `waitid(P_PID)` + SIGCHLD signalfd. SIGCHLD is blocked automatically in that case (upstream no longer requires callers to block it). `send_child_signal()` falls back to `kill()`/`rt_sigqueueinfo()`; `set_child_pidfd_own()` returns `-EOPNOTSUPP` without pidfd. | `src/libsystemd/sd-event/sd-event.c` |
|
||||
| `pam_systemd` session creation | pidfd (5.3) | **revert** of part of `6e14c46bac`: fall back to `CreateSession` (PID based). | `src/login/pam_systemd.c` |
|
||||
| Unit cgroup-empty tracking (`unit_check_cgroup_events()`) | `frozen` key in `cgroup.events` (5.2) | **new**: if the key is missing, read only `populated`. Without this every cgroup event read failed and stopped units were never noticed as empty. | `src/core/cgroup.c` |
|
||||
| Freeze/thaw | cgroup v2 freezer (5.2) | **revert** of `3c86a935ad`: `cg_freezer_supported()` restored; freeze is `-EOPNOTSUPP` / no-op when `cgroup.freeze` is absent. | `src/basic/cgroup-util.[ch]`, `src/core/cgroup.c`, `src/core/unit.c` |
|
||||
| Credentials (`LoadCredential=`, `SetCredential=`, `ImportCredential=` — used by many stock units) | new mount API `fsopen`/`fsmount`/`move_mount` (5.2) | **new**: if the credentials fs cannot be created because the API is missing, use the existing "plain directory" fallback (previously only used on EPERM). | `src/core/exec-credential.c` |
|
||||
| `ProtectProc=` / `ProcSubset=` (used by logind, networkd, timesyncd, hostnamed, …) | per-instance procfs, textual `hidepid=`, `subset=` (5.8) | **new**: on kernels < 5.8 the options are ignored (they would either fail with EINVAL or, worse, change the host's shared `/proc` superblock). `/proc/sys` & friends are then still protected by the normal ProtectKernel* mounts. Upstream removed the old fallback in `0cba5bdcf1`. | `src/core/namespace.c` |
|
||||
| `RootDirectory=` | `open_tree()` (5.2) | **new**: tolerate ENOSYS when cloning the root directory mount and use the classic bind mount path. | `src/core/namespace.c` |
|
||||
| `futimens_opath()`, `access_fd()` | `AT_EMPTY_PATH` for `utimensat()`/`faccessat()` (5.8) | **revert** of `52bf232e82` (manually): fall back to `/proc/self/fd/<fd>`. | `src/basic/fs-util.c` |
|
||||
|
||||
## Networking
|
||||
|
||||
| Area | Kernel feature | Change | Where |
|
||||
|---|---|---|---|
|
||||
| `socket_bind_to_ifindex()` (DHCP client, resolved, networkd, …) | `SO_BINDTOIFINDEX` (5.1) | **revert** of `ddbbc98bd5` (manually): fall back to `SO_BINDTODEVICE`. | `src/basic/socket-util.c` |
|
||||
| `rtnl_resolve_ifname_full()` (alternative names) | `IFLA_ALT_IFNAME` lookup (5.5) | **revert** of `42b09813f4`: treat EINVAL like ENODEV. | `src/libsystemd/sd-netlink/netlink-util.c` |
|
||||
| networkd startup enumeration | nexthop (5.3) | **revert** of `034b69d8b8` (manually): ignore `-EOPNOTSUPP` from nexthop enumeration, otherwise networkd refuses to start. | `src/network/networkd-manager.c` |
|
||||
| networkd permanent MAC address | `IFLA_PERM_ADDRESS` (5.6) | **revert** of `04f05d9bd7`: fall back to ethtool. | `src/network/networkd-link.c` |
|
||||
| IPv6 DNAT (nspawn port forwarding / firewall-util) | nft concatenation fix (5.8) | **revert** of `58fb46ecd5` (manually): `-EOVERFLOW` ⇒ `-EOPNOTSUPP`. | `src/shared/firewall-util.c` |
|
||||
|
||||
## Other
|
||||
|
||||
| Area | Kernel feature | Change | Where |
|
||||
|---|---|---|---|
|
||||
| `SurviveFinalKillSignal=` | `user.*` xattrs on cgroupfs (5.7) | **revert** of `8292b859f3`: fall back to `trusted.survive_final_kill_signal`. | `src/core/cgroup.c`, `src/shared/killall.c` |
|
||||
| Loop devices via `LOOP_SET_STATUS64` fallback | buffer cache invalidation bug (fixed in 5.0) | **revert** of `5e3887e6c1`: issue `BLKFLSBUF` when offset/size is set. | `src/shared/loop-util.c` |
|
||||
|
||||
## Known remaining gaps (not addressed)
|
||||
|
||||
- **cgroup v1 is gone upstream**: the kernel must boot with a cgroup v2 hierarchy. 4.19's cgroup v2
|
||||
lacks the `cpuset` (5.0) and `freezer` (5.2) controllers, `cgroup.kill` (5.14, already graceful).
|
||||
- **PSI** (4.20): `systemd-oomd` does not work; memory-pressure event sources are already optional.
|
||||
- **BPF**: features needing BPF links / BPF LSM / bounded loops (5.3–5.7) — `RestrictFileSystems=`,
|
||||
`RestrictNetworkInterfaces=`, `SocketBind*=`, `BindNetworkInterface=`, … — report "not supported"
|
||||
(upstream probing, `ef12eadfc8`).
|
||||
- **New mount API without fallback** (5.2/5.12): `PrivateBPF=`, `ExtensionImages=`/`ExtensionDirectories=`
|
||||
(overlay via `fsopen`), `MountImages=`, runtime `systemctl bind`/`mount-image`, idmapped mounts,
|
||||
mountfsd, nsresourced, sysext/confext, homed (also needs fscrypt v2 / 5.4), coredump's mount of
|
||||
the crashed process' root.
|
||||
- **`MS_NOSYMFOLLOW`** (5.10) is passed as a legacy `mount(2)` flag; older kernels silently ignore
|
||||
unknown flags, so the mount just lacks `nosymfollow`.
|
||||
- **Credentials** use a plain directory instead of a dedicated (noswap) tmpfs, i.e. less isolation.
|
||||
- **Per-device IO weight** (`IODeviceWeight=`, BFQ per-device weight, 5.4): upstream dropped the
|
||||
EINVAL workaround (`00c4851af0`), expect warnings when used.
|
||||
- **udev net_id** for IP-over-InfiniBand (`dev_port`, 4.20): `d3d375a686` not reverted; multi-port
|
||||
IPoIB devices may get colliding names.
|
||||
- `test-event`'s `test_pidfd` inherently requires pidfds and fails without them.
|
||||
- Anything else newer than 5.10 listed in `README` as "recommended baseline" is expected to fall
|
||||
back gracefully upstream, but that has not been verified on a real old kernel.
|
||||
|
||||
## Testing done
|
||||
|
||||
- Full native test suite (`meson test -C build`) on a 7.x kernel.
|
||||
- Selected tests (`test-stat-util`, `test-mountpoint-util`, `test-chase`, `test-fs-util`,
|
||||
`test-fd-util`, `test-pidref`, `test-process-util`, `test-socket-util`, `test-mount-util`,
|
||||
`test-udev-spawn`, `test-async`, `test-event` except `test_pidfd`) and `systemd --test --system`
|
||||
(with the built generators) under an `LD_PRELOAD` shim that simulates an old kernel: strips
|
||||
`STATX_MNT_ID*`/`STATX_ATTR_MOUNT_ROOT` from `statx()` results and makes `pidfd_open`,
|
||||
`pidfd_send_signal`, `pidfd_spawn`, `close_range` and the new mount API return ENOSYS.
|
||||
- **Not** booted on a real 4.19 kernel yet.
|
||||
@@ -253,6 +253,21 @@ usb:v17CCp1130*
|
||||
usb:v17CCp2420*
|
||||
ID_AV_PRODUCTION_CONTROLLER=1
|
||||
|
||||
####################
|
||||
# PCPanel
|
||||
####################
|
||||
# PCPanel Mini
|
||||
usb:v0483pA3C4*
|
||||
ID_AV_PRODUCTION_CONTROLLER=1
|
||||
|
||||
# PCPanel Pro
|
||||
usb:v0483pA3C5*
|
||||
ID_AV_PRODUCTION_CONTROLLER=1
|
||||
|
||||
# PCPanel RGB
|
||||
usb:v04D8pEB52*
|
||||
ID_AV_PRODUCTION_CONTROLLER=1
|
||||
|
||||
####################
|
||||
# Pioneer
|
||||
####################
|
||||
|
||||
@@ -1209,6 +1209,10 @@ manpages = [
|
||||
'ENABLE_RANDOMSEED'],
|
||||
['systemd-remount-fs.service', '8', ['systemd-remount-fs'], ''],
|
||||
['systemd-repart', '8', ['systemd-repart.service'], 'ENABLE_REPART'],
|
||||
['systemd-report-diskspace@.service',
|
||||
'8',
|
||||
['systemd-report-diskspace', 'systemd-report-diskspace.socket'],
|
||||
'HAVE_LIBMOUNT'],
|
||||
['systemd-report-files@.service',
|
||||
'8',
|
||||
['systemd-report-files', 'systemd-report-files.socket'],
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<?xml version="1.0"?> <!--*-nxml-*-->
|
||||
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
|
||||
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
|
||||
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
|
||||
|
||||
<refentry id="systemd-report-diskspace_.service" xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||
conditional='HAVE_LIBMOUNT'>
|
||||
|
||||
<refentryinfo>
|
||||
<title>systemd-report-diskspace@.service</title>
|
||||
<productname>systemd</productname>
|
||||
</refentryinfo>
|
||||
|
||||
<refmeta>
|
||||
<refentrytitle>systemd-report-diskspace@.service</refentrytitle>
|
||||
<manvolnum>8</manvolnum>
|
||||
</refmeta>
|
||||
|
||||
<refnamediv>
|
||||
<refname>systemd-report-diskspace@.service</refname>
|
||||
<refname>systemd-report-diskspace.socket</refname>
|
||||
<refname>systemd-report-diskspace</refname>
|
||||
<refpurpose>Report size and free disk space of mounted file systems as system report metrics</refpurpose>
|
||||
</refnamediv>
|
||||
|
||||
<refsynopsisdiv>
|
||||
<para><filename>systemd-report-diskspace@.service</filename></para>
|
||||
<para><filename>systemd-report-diskspace.socket</filename></para>
|
||||
<para><filename>/usr/lib/systemd/systemd-report-diskspace</filename></para>
|
||||
</refsynopsisdiv>
|
||||
|
||||
<refsect1>
|
||||
<title>Description</title>
|
||||
|
||||
<para><command>systemd-report-diskspace@.service</command> is a metrics source for
|
||||
<citerefentry><refentrytitle>systemd-report</refentrytitle><manvolnum>1</manvolnum></citerefentry>. It
|
||||
reports the size of the mounted file systems and how much disk space is still available on them, by
|
||||
implementing the <constant>io.systemd.Metrics</constant> Varlink interface on a socket linked into the
|
||||
<filename>/run/systemd/report/</filename> directory.</para>
|
||||
|
||||
<para>All mount points of the system are enumerated. Mount points of file systems that are not backed by
|
||||
a block device (i.e. pseudo file systems such as <literal>proc</literal>, <literal>sysfs</literal> or
|
||||
<literal>tmpfs</literal>, network file systems, <literal>overlay</literal>, …) are skipped, as are
|
||||
read-only mount points. For each remaining mount point two gauge metrics are reported, with the mount
|
||||
point path as metric object:</para>
|
||||
|
||||
<variablelist>
|
||||
<varlistentry>
|
||||
<term><literal>io.systemd.DiskSpace.SizeBytes</literal></term>
|
||||
<listitem><para>The total size of the file system, in bytes. This matches the
|
||||
<literal>1K-blocks</literal> column of
|
||||
<citerefentry project='man-pages'><refentrytitle>df</refentrytitle><manvolnum>1</manvolnum></citerefentry>
|
||||
(scaled to bytes).</para></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><literal>io.systemd.DiskSpace.FreeBytes</literal></term>
|
||||
<listitem><para>The disk space still available to unprivileged users on the file system, in
|
||||
bytes. This matches the <literal>Avail</literal> column of
|
||||
<citerefentry project='man-pages'><refentrytitle>df</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
|
||||
i.e. space reserved for the superuser (as many file systems do) is not counted as
|
||||
free.</para></listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
|
||||
<para>The following metric fields are set on both metrics:</para>
|
||||
|
||||
<variablelist>
|
||||
<varlistentry>
|
||||
<term><varname>fileSystemType</varname></term>
|
||||
<listitem><para>The file system type, for example <literal>ext4</literal> or
|
||||
<literal>btrfs</literal>. Always set.</para></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><varname>source</varname></term>
|
||||
<listitem><para>The mount source, typically the device node the file system is mounted from, for
|
||||
example <literal>/dev/sda1</literal>. Omitted if unknown.</para></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><varname>originatingSource</varname></term>
|
||||
<listitem><para>If the block device the file system is mounted from is a stacked one (for example a
|
||||
LUKS or other device mapper volume), the device node of the block device it ultimately originates
|
||||
from, for example <literal>/dev/sda2</literal> for a file system mounted from
|
||||
<literal>/dev/mapper/root</literal>. Omitted if the source is not a stacked block device, or its
|
||||
origin cannot be determined. Note that stacked devices backed by multiple block devices (e.g. RAID)
|
||||
are not covered by this.</para></listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
|
||||
<para>A file system that is mounted multiple times (for example via bind mounts, or as multiple
|
||||
subvolumes of the same volume) is reported only once, under the shortest of its mount point paths (and
|
||||
among equally long ones, the alphabetically first). Mount points that are shadowed by another mount on
|
||||
top of them, or that the service cannot access, are not considered.</para>
|
||||
</refsect1>
|
||||
|
||||
<!-- Note: we do not document the command line switches here. The systemd-report-diskspace binary itself
|
||||
should not be invoked by users, but only as a service. Use systemd-report(1) to acquire metrics. -->
|
||||
|
||||
<refsect1>
|
||||
<title>See Also</title>
|
||||
<para><simplelist type="inline">
|
||||
<member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
|
||||
<member><citerefentry><refentrytitle>systemd-report</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
|
||||
<member><citerefentry project='man-pages'><refentrytitle>df</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
|
||||
<member><citerefentry project='man-pages'><refentrytitle>statfs</refentrytitle><manvolnum>2</manvolnum></citerefentry></member>
|
||||
</simplelist></para>
|
||||
</refsect1>
|
||||
|
||||
</refentry>
|
||||
@@ -35,6 +35,7 @@ enable systemd-nsresourced.socket
|
||||
enable systemd-pstore.service
|
||||
enable systemd-report-basic.socket
|
||||
enable systemd-report-cgroup.socket
|
||||
enable systemd-report-diskspace.socket
|
||||
enable systemd-report-files.socket
|
||||
enable systemd-report-hwmon.socket
|
||||
enable systemd-report-sign-plain.socket
|
||||
|
||||
@@ -177,8 +177,8 @@ static void plot_tooltip(const UnitTimes *ut) {
|
||||
assert(ut->name);
|
||||
|
||||
svg("%s:\n", ut->name);
|
||||
svg("Activating: %"PRI_USEC".%.3"PRI_USEC"\n", ut->activating / USEC_PER_SEC, ut->activating % USEC_PER_SEC);
|
||||
svg("Activated: %"PRI_USEC".%.3"PRI_USEC"\n", ut->activated / USEC_PER_SEC, ut->activated % USEC_PER_SEC);
|
||||
svg("Activating: %"PRI_USEC".%.6"PRI_USEC"\n", ut->activating / USEC_PER_SEC, ut->activating % USEC_PER_SEC);
|
||||
svg("Activated: %"PRI_USEC".%.6"PRI_USEC"\n", ut->activated / USEC_PER_SEC, ut->activated % USEC_PER_SEC);
|
||||
|
||||
UnitDependency i;
|
||||
FOREACH_ARGUMENT(i, UNIT_AFTER, UNIT_BEFORE, UNIT_REQUIRES, UNIT_REQUISITE, UNIT_WANTS, UNIT_CONFLICTS, UNIT_UPHOLDS)
|
||||
|
||||
@@ -206,6 +206,23 @@ bool cg_kill_supported(void) {
|
||||
return (supported = false);
|
||||
}
|
||||
|
||||
/* ANCIENT-MOD: cgroup v2 freezer is available since kernel 5.2 */
|
||||
bool cg_freezer_supported(void) {
|
||||
static thread_local int supported = -1;
|
||||
|
||||
if (supported >= 0)
|
||||
return supported;
|
||||
|
||||
if (cg_is_available() <= 0)
|
||||
return (supported = false);
|
||||
|
||||
if (access("/sys/fs/cgroup/init.scope/cgroup.freeze", F_OK) >= 0)
|
||||
return (supported = true);
|
||||
if (errno != ENOENT)
|
||||
log_debug_errno(errno, "Failed to check whether cgroup freezer is available, assuming not: %m");
|
||||
return (supported = false);
|
||||
}
|
||||
|
||||
int cg_enumerate_subgroups(const char *path, DIR **ret) {
|
||||
_cleanup_free_ char *fs = NULL;
|
||||
DIR *d;
|
||||
|
||||
@@ -235,6 +235,7 @@ int cg_mask_from_string(const char *s, CGroupMask *ret);
|
||||
int cg_mask_to_string(CGroupMask mask, char **ret);
|
||||
|
||||
bool cg_kill_supported(void);
|
||||
bool cg_freezer_supported(void);
|
||||
|
||||
DECLARE_STRING_TABLE_LOOKUP(cgroup_controller, CGroupController);
|
||||
|
||||
|
||||
+80
-16
@@ -260,9 +260,10 @@ int close_all_fds_frugal(const int except[], size_t n_except) {
|
||||
|
||||
assert(except || n_except == 0);
|
||||
|
||||
/* This is the inner fallback core of close_all_fds(). This never calls malloc() or so and hence is
|
||||
* safe to be called in signal handler context. Most users should call close_all_fds(), but when we
|
||||
* assume we are called from signal handler context, then use this simpler call instead. */
|
||||
/* This is the inner fallback core of close_all_fds(). This never calls malloc() or opendir() or so
|
||||
* and hence is safe to be called in signal handler context. Most users should call close_all_fds(),
|
||||
* but when we assume we are called from signal handler context, then use this simpler call
|
||||
* instead. */
|
||||
|
||||
max_fd = get_max_fd();
|
||||
if (max_fd < 0)
|
||||
@@ -288,6 +289,44 @@ int close_all_fds_frugal(const int except[], size_t n_except) {
|
||||
return r;
|
||||
}
|
||||
|
||||
int close_all_fds_by_proc(const int except[], size_t n_except) {
|
||||
_cleanup_closedir_ DIR *d = NULL;
|
||||
int r = 0;
|
||||
|
||||
d = opendir("/proc/self/fd");
|
||||
if (!d)
|
||||
return close_all_fds_frugal(except, n_except); /* ultimate fallback if /proc/ is not available */
|
||||
|
||||
FOREACH_DIRENT(de, d, return -errno) {
|
||||
int fd = -EBADF, q;
|
||||
|
||||
if (!IN_SET(de->d_type, DT_LNK, DT_UNKNOWN))
|
||||
continue;
|
||||
|
||||
fd = parse_fd(de->d_name);
|
||||
if (fd < 0)
|
||||
/* Let's better ignore this, just in case */
|
||||
continue;
|
||||
|
||||
if (fd < 3)
|
||||
continue;
|
||||
|
||||
if (fd == dirfd(d))
|
||||
continue;
|
||||
|
||||
if (fd_in_set(fd, except, n_except))
|
||||
continue;
|
||||
|
||||
q = close_nointr(fd);
|
||||
if (q != -EBADF) /* Valgrind has its own FD and doesn't want to have it closed */
|
||||
RET_GATHER(r, q);
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
static bool have_close_range = true; /* Assume we live in the future */
|
||||
|
||||
static int close_all_fds_special_case(const int except[], size_t n_except) {
|
||||
assert(n_except == 0 || except);
|
||||
|
||||
@@ -295,6 +334,9 @@ static int close_all_fds_special_case(const int except[], size_t n_except) {
|
||||
* nicely, since we won't need sorting for them. Returns > 0 if the special casing worked, 0
|
||||
* otherwise. */
|
||||
|
||||
if (!have_close_range)
|
||||
return 0;
|
||||
|
||||
if (n_except == 1 && except[0] < 0) /* Minor optimization: if we only got one fd, and it's invalid,
|
||||
* we got none */
|
||||
n_except = 0;
|
||||
@@ -303,22 +345,31 @@ static int close_all_fds_special_case(const int except[], size_t n_except) {
|
||||
|
||||
case 0:
|
||||
/* Close everything. Yay! */
|
||||
if (close_range(3, INT_MAX, 0) < 0)
|
||||
return -errno;
|
||||
|
||||
return 1;
|
||||
if (close_range(3, INT_MAX, 0) >= 0)
|
||||
return 1;
|
||||
|
||||
if (ERRNO_IS_NOT_SUPPORTED(errno) || ERRNO_IS_PRIVILEGE(errno)) {
|
||||
have_close_range = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -errno;
|
||||
|
||||
case 1:
|
||||
/* Close all but exactly one, then we don't need no sorting. This is a pretty common
|
||||
* case, hence let's handle it specially. */
|
||||
|
||||
if (except[0] > 3 && close_range(3, except[0] - 1, 0) < 0)
|
||||
return -errno;
|
||||
if ((except[0] <= 3 || close_range(3, except[0]-1, 0) >= 0) &&
|
||||
(except[0] >= INT_MAX || close_range(MAX(3, except[0]+1), -1, 0) >= 0))
|
||||
return 1;
|
||||
|
||||
if (except[0] < INT_MAX && close_range(MAX(3, except[0] + 1), -1, 0) < 0)
|
||||
return -errno;
|
||||
if (ERRNO_IS_NOT_SUPPORTED(errno) || ERRNO_IS_PRIVILEGE(errno)) {
|
||||
have_close_range = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
return -errno;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
@@ -350,6 +401,9 @@ int close_all_fds(const int except[], size_t n_except) {
|
||||
if (r > 0) /* special case worked! */
|
||||
return 0;
|
||||
|
||||
if (!have_close_range)
|
||||
return close_all_fds_by_proc(except, n_except);
|
||||
|
||||
_cleanup_free_ int *sorted_malloc = NULL;
|
||||
size_t n_sorted;
|
||||
int *sorted;
|
||||
@@ -369,7 +423,7 @@ int close_all_fds(const int except[], size_t n_except) {
|
||||
sorted = newa(int, n_sorted);
|
||||
|
||||
if (!sorted) /* Fallback on OOM. */
|
||||
return close_all_fds_frugal(except, n_except);
|
||||
return close_all_fds_by_proc(except, n_except);
|
||||
|
||||
memcpy(sorted, except, n_except * sizeof(int));
|
||||
|
||||
@@ -391,8 +445,13 @@ int close_all_fds(const int except[], size_t n_except) {
|
||||
continue;
|
||||
|
||||
/* Close everything between the start and end fds (both of which shall stay open) */
|
||||
if (close_range(start + 1, end - 1, 0) < 0)
|
||||
return -errno;
|
||||
if (close_range(start + 1, end - 1, 0) < 0) {
|
||||
if (!ERRNO_IS_NOT_SUPPORTED(errno) && !ERRNO_IS_PRIVILEGE(errno))
|
||||
return -errno;
|
||||
|
||||
have_close_range = false;
|
||||
return close_all_fds_by_proc(except, n_except);
|
||||
}
|
||||
}
|
||||
|
||||
/* The loop succeeded. Let's now close everything beyond the end */
|
||||
@@ -400,8 +459,13 @@ int close_all_fds(const int except[], size_t n_except) {
|
||||
if (sorted[n_sorted-1] >= INT_MAX) /* Dont let the addition below overflow */
|
||||
return 0;
|
||||
|
||||
if (close_range(sorted[n_sorted-1] + 1, INT_MAX, 0) < 0)
|
||||
return -errno;
|
||||
if (close_range(sorted[n_sorted-1] + 1, INT_MAX, 0) < 0) {
|
||||
if (!ERRNO_IS_NOT_SUPPORTED(errno) && !ERRNO_IS_PRIVILEGE(errno))
|
||||
return -errno;
|
||||
|
||||
have_close_range = false;
|
||||
return close_all_fds_by_proc(except, n_except);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ int get_max_fd(void);
|
||||
|
||||
int close_all_fds(const int except[], size_t n_except);
|
||||
int close_all_fds_without_malloc(const int except[], size_t n_except);
|
||||
int close_all_fds_by_proc(const int except[], size_t n_except);
|
||||
int close_all_fds_frugal(const int except[], size_t n_except);
|
||||
|
||||
int pack_fds(int fds[], size_t n);
|
||||
|
||||
+28
-2
@@ -322,7 +322,20 @@ int futimens_opath(int fd, const struct timespec ts[2]) {
|
||||
|
||||
assert(fd >= 0);
|
||||
|
||||
return RET_NERRNO(utimensat(fd, "", ts, AT_EMPTY_PATH));
|
||||
if (utimensat(fd, "", ts, AT_EMPTY_PATH) >= 0)
|
||||
return 0;
|
||||
if (errno != EINVAL)
|
||||
return -errno;
|
||||
|
||||
/* ANCIENT-MOD: AT_EMPTY_PATH is supported since kernel 5.8, fall back to /proc/self/fd/ */
|
||||
if (utimensat(AT_FDCWD, FORMAT_PROC_FD_PATH(fd), ts, /* flags= */ 0) < 0) {
|
||||
if (errno != ENOENT)
|
||||
return -errno;
|
||||
|
||||
return proc_fd_enoent_errno();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int stat_warn_permissions(const char *path, const struct stat *st) {
|
||||
@@ -371,7 +384,20 @@ int access_fd(int fd, int mode) {
|
||||
|
||||
assert(fd >= 0);
|
||||
|
||||
return RET_NERRNO(faccessat(fd, "", mode, AT_EMPTY_PATH));
|
||||
if (faccessat(fd, "", mode, AT_EMPTY_PATH) >= 0)
|
||||
return 0;
|
||||
if (errno != EINVAL)
|
||||
return -errno;
|
||||
|
||||
/* ANCIENT-MOD: AT_EMPTY_PATH is supported since kernel 5.8, fall back to /proc/self/fd/ */
|
||||
if (access(FORMAT_PROC_FD_PATH(fd), mode) < 0) {
|
||||
if (errno != ENOENT)
|
||||
return -errno;
|
||||
|
||||
return proc_fd_enoent_errno();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int touch_fd(int fd, usec_t stamp) {
|
||||
|
||||
@@ -681,6 +681,8 @@ int mount_option_supported(const char *fstype, const char *key, const char *valu
|
||||
if (fd < 0) {
|
||||
if (errno == ENODEV)
|
||||
return log_debug_errno(errno, "Kernel does not support file system type '%s'.", fstype);
|
||||
if (ERRNO_IS_NOT_SUPPORTED(errno))
|
||||
return -EAGAIN; /* ANCIENT-MOD: new mount API not available (kernel < 5.2) → don't know */
|
||||
|
||||
return log_debug_errno(errno, "Failed to open superblock context for '%s': %m", fstype);
|
||||
}
|
||||
|
||||
+10
-2
@@ -28,8 +28,11 @@ int pidfd_check_pidfs(int pid_fd) {
|
||||
_cleanup_close_ int our_fd = -EBADF;
|
||||
if (pid_fd < 0) {
|
||||
our_fd = pidfd_open(getpid_cached(), /* flags= */ 0);
|
||||
if (our_fd < 0)
|
||||
if (our_fd < 0) {
|
||||
if (ERRNO_IS_NOT_SUPPORTED(errno)) /* ANCIENT-MOD: kernel < 5.3 */
|
||||
return (have_pidfs = false);
|
||||
return -errno;
|
||||
}
|
||||
|
||||
pid_fd = our_fd;
|
||||
}
|
||||
@@ -102,6 +105,8 @@ int pidfd_info_mask_is_supported(uint64_t mask) {
|
||||
|
||||
if (cached == 0) {
|
||||
_cleanup_close_ int pidfd = r = RET_NERRNO(pidfd_open(getpid_cached(), /* flags= */ 0));
|
||||
if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) /* ANCIENT-MOD: kernel < 5.3 */
|
||||
return (cached = -EOPNOTSUPP);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
@@ -334,8 +339,11 @@ int pidfd_get_inode_id_self_cached(uint64_t *ret) {
|
||||
return initialized;
|
||||
|
||||
_cleanup_close_ int fd = pidfd_open(getpid_cached(), 0);
|
||||
if (fd < 0)
|
||||
if (fd < 0) {
|
||||
if (ERRNO_IS_NOT_SUPPORTED(errno)) /* ANCIENT-MOD: kernel < 5.3 */
|
||||
return (initialized = -EOPNOTSUPP);
|
||||
return -errno;
|
||||
}
|
||||
|
||||
r = pidfd_get_inode_id(fd, &cached);
|
||||
if (ERRNO_IS_NEG_NOT_SUPPORTED(r))
|
||||
|
||||
+27
-8
@@ -106,9 +106,10 @@ int pidref_set_pid_full(PidRef *pidref, pid_t pid, unsigned flags) {
|
||||
|
||||
fd = pidfd_open(pid, flags);
|
||||
if (fd < 0) {
|
||||
/* Graceful fallback in case the kernel is out of fds.
|
||||
/* Graceful fallback in case the kernel is out of fds or lacks pidfd_open() (ANCIENT-MOD: < 5.3).
|
||||
* The PIDFD_THREAD flag is supported since kernel v6.9 (64bef697d33b75fc06c5789b3f8108680271529f). */
|
||||
if (!(ERRNO_IS_RESOURCE(errno) || (errno == EINVAL && FLAGS_SET(flags, PIDFD_THREAD))))
|
||||
if (!(ERRNO_IS_RESOURCE(errno) || ERRNO_IS_NOT_SUPPORTED(errno) ||
|
||||
(errno == EINVAL && FLAGS_SET(flags, PIDFD_THREAD))))
|
||||
return log_debug_errno(errno, "Failed to open pidfd for pid " PID_FMT ": %m", pid);
|
||||
|
||||
fd = -EBADF;
|
||||
@@ -468,16 +469,34 @@ int pidref_wait_for_terminate_full(PidRef *pidref, usec_t timeout, siginfo_t *re
|
||||
if (pidref->pid == 1 || pidref_is_self(pidref))
|
||||
return -ECHILD;
|
||||
|
||||
if (pidref->fd < 0 && (timeout != USEC_INFINITY || fiber_ops_is_set()))
|
||||
return -ENOMEDIUM;
|
||||
|
||||
usec_t ts = timeout == USEC_INFINITY ? USEC_INFINITY : usec_add(now(CLOCK_MONOTONIC), timeout);
|
||||
|
||||
/* ANCIENT-MOD: no pidfd (kernel < 5.3), poll with waitid(WNOHANG) to honour the timeout */
|
||||
if (pidref->fd < 0 && ts != USEC_INFINITY)
|
||||
for (;;) {
|
||||
siginfo_t si = {};
|
||||
|
||||
r = RET_NERRNO(waitid(P_PID, pidref->pid, &si, WEXITED|WNOHANG));
|
||||
if (r == -EINTR)
|
||||
continue;
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (si.si_pid != 0) {
|
||||
if (ret_si)
|
||||
*ret_si = si;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (now(CLOCK_MONOTONIC) >= ts)
|
||||
return -ETIMEDOUT;
|
||||
|
||||
(void) usleep_safe(10 * USEC_PER_MSEC);
|
||||
}
|
||||
|
||||
/* Poll the pidfd before waitid() if either there's a finite timeout (so we can honor it) or
|
||||
* we're on a fiber (so fd_wait_for_event() can suspend us instead of blocking the event loop
|
||||
* inside waitid()). Otherwise let waitid() block directly. The precondition above guarantees
|
||||
* pidref->fd >= 0 in both cases. */
|
||||
bool poll_first = ts != USEC_INFINITY || fiber_ops_is_set();
|
||||
* inside waitid()). Otherwise let waitid() block directly. */
|
||||
bool poll_first = pidref->fd >= 0 && (ts != USEC_INFINITY || fiber_ops_is_set());
|
||||
|
||||
for (;;) {
|
||||
if (poll_first) {
|
||||
|
||||
+13
-1
@@ -1419,13 +1419,25 @@ int socket_bind_to_ifname(int fd, const char *ifname) {
|
||||
}
|
||||
|
||||
int socket_bind_to_ifindex(int fd, int ifindex) {
|
||||
char ifname[IF_NAMESIZE];
|
||||
int r;
|
||||
|
||||
assert(fd >= 0);
|
||||
|
||||
if (ifindex <= 0)
|
||||
/* Drop binding */
|
||||
return RET_NERRNO(setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, NULL, 0));
|
||||
|
||||
return setsockopt_int(fd, SOL_SOCKET, SO_BINDTOIFINDEX, ifindex);
|
||||
r = setsockopt_int(fd, SOL_SOCKET, SO_BINDTOIFINDEX, ifindex);
|
||||
if (r != -ENOPROTOOPT)
|
||||
return r;
|
||||
|
||||
/* ANCIENT-MOD: fall back to SO_BINDTODEVICE on kernels < 5.1 which didn't have SO_BINDTOIFINDEX */
|
||||
r = format_ifname(ifindex, ifname);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return socket_bind_to_ifname(fd, ifname);
|
||||
}
|
||||
|
||||
int socket_autobind(int fd, char **ret_name) {
|
||||
|
||||
@@ -11,14 +11,17 @@
|
||||
#include "dirent-util.h"
|
||||
#include "errno-util.h"
|
||||
#include "fd-util.h"
|
||||
#include "fileio.h"
|
||||
#include "filesystems.h"
|
||||
#include "fs-util.h"
|
||||
#include "hash-funcs.h"
|
||||
#include "log.h"
|
||||
#include "mountpoint-util.h"
|
||||
#include "parse-util.h"
|
||||
#include "path-util.h"
|
||||
#include "siphash24.h"
|
||||
#include "stat-util.h"
|
||||
#include "stdio-util.h"
|
||||
#include "string-util.h"
|
||||
#include "time-util.h"
|
||||
|
||||
@@ -395,6 +398,102 @@ static const char* statx_attribute_to_name(uint64_t attr);
|
||||
DEFINE_STATX_BITS_TO_STRING(statx_mask, unsigned, statx_mask_one_to_name, "0x%x");
|
||||
DEFINE_STATX_BITS_TO_STRING(statx_attributes, uint64_t, statx_attribute_to_name, "0x%" PRIx64);
|
||||
|
||||
/* ANCIENT-MOD: emulation of STATX_MNT_ID and STATX_ATTR_MOUNT_ROOT for kernels < 5.8 */
|
||||
static int fd_fdinfo_mnt_id(int fd, uint64_t *ret) {
|
||||
char path[STRLEN("/proc/self/fdinfo/") + DECIMAL_STR_MAX(int)];
|
||||
_cleanup_free_ char *p = NULL;
|
||||
int r;
|
||||
|
||||
assert(fd >= 0);
|
||||
assert(ret);
|
||||
|
||||
xsprintf(path, "/proc/self/fdinfo/%i", fd);
|
||||
|
||||
r = get_proc_field(path, "mnt_id", &p);
|
||||
if (r == -ENOENT)
|
||||
return proc_fd_enoent_errno();
|
||||
if (r == -ENODATA)
|
||||
return -EUNATCH;
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return safe_atou64(p, ret);
|
||||
}
|
||||
|
||||
static int fd_open_parent(int fd, const struct statx *sx) {
|
||||
_cleanup_free_ char *p = NULL, *d = NULL;
|
||||
int r;
|
||||
|
||||
if (FLAGS_SET(sx->stx_mask, STATX_TYPE) ? S_ISDIR(sx->stx_mode) : fd_verify_directory(fd) >= 0)
|
||||
return RET_NERRNO(openat(fd, "..", O_PATH|O_DIRECTORY|O_CLOEXEC));
|
||||
|
||||
r = fd_get_path(fd, &p);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = path_extract_directory(p, &d);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return RET_NERRNO(open(d, O_PATH|O_DIRECTORY|O_CLOEXEC));
|
||||
}
|
||||
|
||||
static int xstatx_emulate(
|
||||
int fd,
|
||||
const char *path,
|
||||
int statx_flags,
|
||||
bool want_mnt_id,
|
||||
bool want_mount_root,
|
||||
struct statx *sx) {
|
||||
|
||||
_cleanup_close_ int target_fd = -EBADF, parent_fd = -EBADF;
|
||||
uint64_t mnt_id, parent_mnt_id;
|
||||
int r;
|
||||
|
||||
assert(sx);
|
||||
|
||||
if (isempty(path) && fd >= 0)
|
||||
target_fd = RET_NERRNO(fcntl(fd, F_DUPFD_CLOEXEC, 3));
|
||||
else
|
||||
target_fd = RET_NERRNO(openat(fd, isempty(path) ? "." : path,
|
||||
O_PATH|O_CLOEXEC|(FLAGS_SET(statx_flags, AT_SYMLINK_NOFOLLOW) ? O_NOFOLLOW : 0)));
|
||||
if (target_fd < 0)
|
||||
return target_fd;
|
||||
|
||||
r = fd_fdinfo_mnt_id(target_fd, &mnt_id);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (want_mnt_id) {
|
||||
sx->stx_mnt_id = mnt_id;
|
||||
sx->stx_mask |= STATX_MNT_ID;
|
||||
}
|
||||
|
||||
if (!want_mount_root)
|
||||
return 0;
|
||||
|
||||
bool is_root = false;
|
||||
parent_fd = fd_open_parent(target_fd, sx);
|
||||
if (parent_fd < 0)
|
||||
log_debug_errno(parent_fd, "Failed to open parent to determine mount root, assuming not a mount root: %m");
|
||||
else {
|
||||
struct stat a, b;
|
||||
|
||||
r = fd_fdinfo_mnt_id(parent_fd, &parent_mnt_id);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (mnt_id != parent_mnt_id)
|
||||
is_root = true;
|
||||
else if (fstat(target_fd, &a) >= 0 && fstat(parent_fd, &b) >= 0)
|
||||
is_root = stat_inode_same(&a, &b);
|
||||
}
|
||||
|
||||
SET_FLAG(sx->stx_attributes, STATX_ATTR_MOUNT_ROOT, is_root);
|
||||
sx->stx_attributes_mask |= STATX_ATTR_MOUNT_ROOT;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int xstatx_full(int fd,
|
||||
const char *path,
|
||||
int statx_flags,
|
||||
@@ -439,6 +538,18 @@ int xstatx_full(int fd,
|
||||
&sx) < 0)
|
||||
return negative_errno();
|
||||
|
||||
bool want_mnt_id =
|
||||
(FLAGS_SET(xstatx_flags, XSTATX_MNT_ID_BEST) && !(sx.stx_mask & (STATX_MNT_ID|STATX_MNT_ID_UNIQUE))) ||
|
||||
(FLAGS_SET(request_mask, STATX_MNT_ID) && !FLAGS_SET(sx.stx_mask, STATX_MNT_ID));
|
||||
bool want_mount_root =
|
||||
FLAGS_SET(mandatory_attributes, STATX_ATTR_MOUNT_ROOT) &&
|
||||
!FLAGS_SET(sx.stx_attributes_mask, STATX_ATTR_MOUNT_ROOT);
|
||||
if (want_mnt_id || want_mount_root) {
|
||||
r = xstatx_emulate(fd, path, statx_flags, want_mnt_id, want_mount_root, &sx);
|
||||
if (r < 0)
|
||||
log_debug_errno(r, "Failed to emulate statx() mount fields, ignoring: %m");
|
||||
}
|
||||
|
||||
if (FLAGS_SET(xstatx_flags, XSTATX_MNT_ID_BEST) &&
|
||||
!(sx.stx_mask & (STATX_MNT_ID|STATX_MNT_ID_UNIQUE)))
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EUNATCH), "statx() did not return either STATX_MNT_ID or STATX_MNT_ID_UNIQUE.");
|
||||
|
||||
@@ -2681,17 +2681,32 @@ int terminal_get_size(
|
||||
return r;
|
||||
}
|
||||
|
||||
static int fd_is_vc(int fd) {
|
||||
_cleanup_free_ char *name = NULL;
|
||||
int r;
|
||||
|
||||
assert(fd >= 0);
|
||||
|
||||
r = getttyname_harder(fd, &name);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return tty_is_vc_resolve(name);
|
||||
}
|
||||
|
||||
int terminal_fix_size(int input_fd, int output_fd) {
|
||||
unsigned rows, columns;
|
||||
int r;
|
||||
|
||||
/* Tries to update the current terminal dimensions to the ones reported via ANSI sequences.
|
||||
*
|
||||
* Why bother with this? The ioctl() information is often incorrect on serial terminals (since
|
||||
* there's no handshake or protocol to determine the right dimensions in RS232), but since the ANSI
|
||||
* sequences are interpreted by the final terminal instead of an intermediary tty driver they should
|
||||
* be more accurate.
|
||||
*/
|
||||
/* Don't ask a virtual console at all. There is no intermediary here that could be wrong about the
|
||||
* dimensions: the kernel renders the screen itself, so its winsize is authoritative. */
|
||||
r = fd_is_vc(input_fd);
|
||||
if (r < 0)
|
||||
log_debug_errno(r, "Failed to determine whether terminal is a virtual console, assuming it is not: %m");
|
||||
else if (r > 0) {
|
||||
log_debug("Not querying dimensions of a virtual console, kernel dimensions are authoritative.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct winsize ws = {};
|
||||
if (ioctl(output_fd, TIOCGWINSZ, &ws) < 0)
|
||||
|
||||
+41
-4
@@ -937,12 +937,39 @@ static void cgroup_delegate_xattr_apply(Unit *u) {
|
||||
}
|
||||
|
||||
static void cgroup_survive_xattr_apply(Unit *u) {
|
||||
int r;
|
||||
|
||||
assert(u);
|
||||
|
||||
if (u->survive_final_kill_signal)
|
||||
unit_set_xattr_graceful(u, "user.survive_final_kill_signal", "1", 1);
|
||||
else
|
||||
CGroupRuntime *crt = unit_get_cgroup_runtime(u);
|
||||
if (!crt)
|
||||
return;
|
||||
|
||||
if (u->survive_final_kill_signal) {
|
||||
r = cg_set_xattr(
|
||||
crt->cgroup_path,
|
||||
"user.survive_final_kill_signal",
|
||||
"1",
|
||||
1,
|
||||
/* flags= */ 0);
|
||||
/* user xattr support was added in kernel v5.7 */
|
||||
if (ERRNO_IS_NEG_NOT_SUPPORTED(r))
|
||||
r = cg_set_xattr(
|
||||
crt->cgroup_path,
|
||||
"trusted.survive_final_kill_signal",
|
||||
"1",
|
||||
1,
|
||||
/* flags= */ 0);
|
||||
if (r < 0)
|
||||
log_unit_debug_errno(u,
|
||||
r,
|
||||
"Failed to set 'survive_final_kill_signal' xattr on control "
|
||||
"group %s, ignoring: %m",
|
||||
empty_to_root(crt->cgroup_path));
|
||||
} else {
|
||||
unit_remove_xattr_graceful(u, "user.survive_final_kill_signal");
|
||||
unit_remove_xattr_graceful(u, "trusted.survive_final_kill_signal");
|
||||
}
|
||||
}
|
||||
|
||||
static void cgroup_xattr_apply(Unit *u) {
|
||||
@@ -3189,6 +3216,13 @@ static int unit_check_cgroup_events(Unit *u) {
|
||||
"cgroup.events",
|
||||
STRV_MAKE("populated", "frozen"),
|
||||
values);
|
||||
if (r == -ENXIO)
|
||||
/* ANCIENT-MOD: the "frozen" key exists only since kernel 5.2 (cgroup v2 freezer) */
|
||||
r = cg_get_keyed_attribute(
|
||||
crt->cgroup_path,
|
||||
"cgroup.events",
|
||||
STRV_MAKE("populated"),
|
||||
values);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
@@ -3203,7 +3237,7 @@ static int unit_check_cgroup_events(Unit *u) {
|
||||
/* Disregard freezer state changes due to operations not initiated by us.
|
||||
* See: https://github.com/systemd/systemd/pull/13512/files#r416469963 and
|
||||
* https://github.com/systemd/systemd/pull/13512#issuecomment-573007207 */
|
||||
if (IN_SET(u->freezer_state, FREEZER_FREEZING, FREEZER_FREEZING_BY_PARENT, FREEZER_THAWING))
|
||||
if (values[1] && IN_SET(u->freezer_state, FREEZER_FREEZING, FREEZER_FREEZING_BY_PARENT, FREEZER_THAWING))
|
||||
unit_freezer_complete(u, streq(values[1], "0") ? FREEZER_RUNNING : FREEZER_FROZEN);
|
||||
|
||||
free_many_charp(values, ELEMENTSOF(values));
|
||||
@@ -4122,6 +4156,9 @@ int unit_cgroup_freezer_action(Unit *u, FreezerAction action) {
|
||||
assert(action >= 0);
|
||||
assert(action < _FREEZER_ACTION_MAX);
|
||||
|
||||
if (!cg_freezer_supported())
|
||||
return 0;
|
||||
|
||||
unit_next_freezer_state(u, action, &next, &objective);
|
||||
|
||||
CGroupRuntime *crt = unit_get_cgroup_runtime(u);
|
||||
|
||||
@@ -1052,8 +1052,9 @@ static int setup_credentials_internal(
|
||||
}
|
||||
|
||||
mfd = fsmount_credentials_fs(&fs_fd);
|
||||
if (ERRNO_IS_NEG_PRIVILEGE(mfd) && !dir_mounted) {
|
||||
log_debug_errno(mfd, "Lacking privilege to mount credentials fs, falling back to plain directory.");
|
||||
if ((ERRNO_IS_NEG_PRIVILEGE(mfd) || ERRNO_IS_NEG_NOT_SUPPORTED(mfd)) && !dir_mounted) {
|
||||
/* ANCIENT-MOD: also fall back if the new mount API is unavailable (kernel < 5.2) */
|
||||
log_debug_errno(mfd, "Lacking privilege or kernel support to mount credentials fs, falling back to plain directory.");
|
||||
return setup_credentials_plain_dir(context, cred_dir);
|
||||
}
|
||||
if (mfd < 0)
|
||||
|
||||
+28
-4
@@ -7,6 +7,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/utsname.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "alloc-util.h"
|
||||
@@ -1562,6 +1563,25 @@ static int mount_private_cgroup2fs(const MountEntry *m, const NamespaceParameter
|
||||
return mount_private_apivfs("cgroup2", mount_entry_path(m), "/sys/fs/cgroup", /* opts= */ NULL, p->runtime_scope);
|
||||
}
|
||||
|
||||
/* ANCIENT-MOD: before kernel 5.8 procfs options are per pid namespace (shared with the host /proc) and
|
||||
* textual hidepid= / subset= do not exist, hence ignore ProtectProc=/ProcSubset= there. */
|
||||
static bool procfs_per_instance_options_supported(void) {
|
||||
static int cached = -1;
|
||||
struct utsname uts;
|
||||
|
||||
if (cached >= 0)
|
||||
return cached;
|
||||
|
||||
if (uname(&uts) < 0)
|
||||
return (cached = true);
|
||||
|
||||
cached = strverscmp_improved(uts.release, "5.8") >= 0;
|
||||
if (!cached)
|
||||
log_debug("Kernel %s does not support per-instance procfs options, ignoring ProtectProc=/ProcSubset=.", uts.release);
|
||||
|
||||
return cached;
|
||||
}
|
||||
|
||||
static int mount_procfs(const MountEntry *m, const NamespaceParameters *p) {
|
||||
_cleanup_free_ char *opts = NULL;
|
||||
int r;
|
||||
@@ -1569,8 +1589,9 @@ static int mount_procfs(const MountEntry *m, const NamespaceParameters *p) {
|
||||
assert(m);
|
||||
assert(p);
|
||||
|
||||
if (p->protect_proc != PROTECT_PROC_DEFAULT ||
|
||||
p->proc_subset != PROC_SUBSET_ALL) {
|
||||
if ((p->protect_proc != PROTECT_PROC_DEFAULT ||
|
||||
p->proc_subset != PROC_SUBSET_ALL) &&
|
||||
procfs_per_instance_options_supported()) {
|
||||
|
||||
opts = strjoin("hidepid=",
|
||||
p->protect_proc == PROTECT_PROC_DEFAULT ? "off" : protect_proc_to_string(p->protect_proc));
|
||||
@@ -2663,7 +2684,9 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) {
|
||||
* as well now. */
|
||||
.propagation = MS_SLAVE,
|
||||
});
|
||||
if (_root_mount_fd < 0 && !ERRNO_IS_NEG_PRIVILEGE(_root_mount_fd) && _root_mount_fd != -EINVAL)
|
||||
/* ANCIENT-MOD: also tolerate missing open_tree() (kernel < 5.2) */
|
||||
if (_root_mount_fd < 0 && !ERRNO_IS_NEG_PRIVILEGE(_root_mount_fd) &&
|
||||
!ERRNO_IS_NEG_NOT_SUPPORTED(_root_mount_fd) && _root_mount_fd != -EINVAL)
|
||||
return log_debug_errno(_root_mount_fd, "Failed to clone specified directory: %m");
|
||||
|
||||
root_mount_fd = _root_mount_fd;
|
||||
@@ -2866,7 +2889,8 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) {
|
||||
/* In case /proc is successfully mounted with pid tree subset only (ProcSubset=pid), the protective
|
||||
mounts to non-pid /proc paths would fail. But the pid only option may have failed gracefully, so
|
||||
let's try the mounts but it's not fatal if they don't succeed. */
|
||||
bool ignore_protect_proc = p->ignore_protect_paths || p->proc_subset == PROC_SUBSET_PID;
|
||||
bool ignore_protect_proc = p->ignore_protect_paths ||
|
||||
(p->proc_subset == PROC_SUBSET_PID && procfs_per_instance_options_supported());
|
||||
if (p->protect_kernel_tunables) {
|
||||
r = append_static_mounts(&ml,
|
||||
protect_kernel_tunables_proc_table,
|
||||
|
||||
+1
-1
@@ -6654,7 +6654,7 @@ int unit_freezer_action(Unit *u, FreezerAction action) {
|
||||
assert(u);
|
||||
assert(IN_SET(action, FREEZER_FREEZE, FREEZER_THAW));
|
||||
|
||||
if (!unit_can_freeze(u))
|
||||
if (!cg_freezer_supported() || !unit_can_freeze(u))
|
||||
return -EOPNOTSUPP;
|
||||
|
||||
if (u->job)
|
||||
|
||||
@@ -123,14 +123,12 @@ int bus_call_suspend(
|
||||
if (r < 0)
|
||||
return sd_bus_error_set_errno(reterr_error, r);
|
||||
|
||||
/* An interruption takes precedence even if the reply arrived in the same tick: the wait
|
||||
* consumed the queued cancellation or timeout, and returning the reply instead would drop it. */
|
||||
r = sd_fiber_await(f);
|
||||
|
||||
/* If the future isn't resolved, the suspend was interrupted before a reply arrived (fiber
|
||||
* cancelled, fiber-wide SD_FIBER_TIMEOUT scope expired, …). There's no reply to extract,
|
||||
* so surface the resume error directly. When the future is resolved, future_get_bus_reply()
|
||||
* recovers either the reply or the detailed sd_bus_error from the error reply. */
|
||||
if (sd_future_state(f) != SD_FUTURE_RESOLVED)
|
||||
if (r < 0)
|
||||
return sd_bus_error_set_errno(reterr_error, r);
|
||||
|
||||
/* Recovers either the reply or the detailed sd_bus_error from the error reply. */
|
||||
return future_get_bus_reply(f, reterr_error, ret_reply);
|
||||
}
|
||||
|
||||
@@ -299,6 +299,7 @@ static int interrupted_calls_fiber(void *userdata) {
|
||||
}
|
||||
|
||||
ASSERT_OK_ZERO(sd_fiber_await(caller));
|
||||
ASSERT_OK_ZERO(sd_future_result(caller));
|
||||
ASSERT_NULL(s->c->pending_call);
|
||||
ASSERT_NULL(s->c->waiter);
|
||||
}
|
||||
|
||||
@@ -189,6 +189,8 @@ static int request_exit(void *userdata) {
|
||||
/* Wait for all client fibers to complete before requesting exit */
|
||||
ASSERT_OK(sd_fiber_await(args->client1));
|
||||
ASSERT_OK(sd_fiber_await(args->client2));
|
||||
ASSERT_OK(sd_future_result(args->client1));
|
||||
ASSERT_OK(sd_future_result(args->client2));
|
||||
|
||||
ASSERT_OK(sd_bus_new(&bus));
|
||||
|
||||
|
||||
@@ -411,6 +411,10 @@ int event_run_suspend(sd_event *e, uint64_t timeout) {
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = sd_future_result(group);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = sd_event_prepare(e);
|
||||
if (r == 0)
|
||||
r = sd_event_wait(e, 0);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <linux/magic.h>
|
||||
#include <malloc.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/timerfd.h>
|
||||
#include <sys/wait.h>
|
||||
#include <threads.h>
|
||||
@@ -51,9 +52,20 @@ static bool EVENT_SOURCE_WATCH_PIDFD(const sd_event_source *s) {
|
||||
/* Returns true if this is a PID event source and can be implemented by watching EPOLLIN */
|
||||
return s &&
|
||||
s->type == SOURCE_CHILD &&
|
||||
s->child.pidfd >= 0 &&
|
||||
(s->child.options & ~WNOWAIT) == WEXITED;
|
||||
}
|
||||
|
||||
/* ANCIENT-MOD: child sources without pidfd (kernel < 5.3) fall back to waitid(P_PID) */
|
||||
static int child_waitid(sd_event_source *s, siginfo_t *si, int options) {
|
||||
assert(s);
|
||||
|
||||
if (s->child.pidfd >= 0)
|
||||
return waitid(P_PIDFD, s->child.pidfd, si, options);
|
||||
|
||||
return waitid(P_PID, s->child.pid, si, options);
|
||||
}
|
||||
|
||||
static bool event_source_is_online(sd_event_source *s) {
|
||||
assert(s);
|
||||
return s->enabled != SD_EVENT_OFF && !s->ratelimited;
|
||||
@@ -1098,10 +1110,12 @@ static sd_event_source* source_free(sd_event_source *s) {
|
||||
|
||||
if (s->child.process_owned) {
|
||||
assert(s->child.pid > 0);
|
||||
assert(s->child.pidfd >= 0);
|
||||
|
||||
if (!s->child.exited) {
|
||||
r = RET_NERRNO(pidfd_send_signal(s->child.pidfd, SIGKILL, NULL, 0));
|
||||
if (s->child.pidfd >= 0)
|
||||
r = RET_NERRNO(pidfd_send_signal(s->child.pidfd, SIGKILL, NULL, 0));
|
||||
else
|
||||
r = RET_NERRNO(kill(s->child.pid, SIGKILL));
|
||||
if (r < 0 && r != -ESRCH)
|
||||
log_debug_errno(r, "Failed to kill process " PID_FMT ", ignoring: %m",
|
||||
s->child.pid);
|
||||
@@ -1111,7 +1125,7 @@ static sd_event_source* source_free(sd_event_source *s) {
|
||||
siginfo_t si = {};
|
||||
|
||||
/* Reap the child if we can */
|
||||
(void) waitid(P_PIDFD, s->child.pidfd, &si, WEXITED);
|
||||
(void) child_waitid(s, &si, WEXITED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1648,10 +1662,22 @@ _public_ int sd_event_add_child(
|
||||
* PID, and make regular waitid() handling race-free. */
|
||||
|
||||
s->child.pidfd = pidfd_open(pid, 0);
|
||||
if (s->child.pidfd < 0)
|
||||
return -errno;
|
||||
if (s->child.pidfd < 0) {
|
||||
if (!ERRNO_IS_NOT_SUPPORTED(errno) && !ERRNO_IS_PRIVILEGE(errno))
|
||||
return -errno;
|
||||
|
||||
s->child.pidfd_owned = true; /* If we allocate the pidfd we own it by default */
|
||||
/* Without pidfd we depend on SIGCHLD via signalfd, which only works if it is blocked */
|
||||
r = signal_is_blocked(SIGCHLD);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r == 0) {
|
||||
log_debug("pidfd_open() unavailable, blocking SIGCHLD to watch child processes.");
|
||||
r = sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
} else
|
||||
s->child.pidfd_owned = true; /* If we allocate the pidfd we own it by default */
|
||||
|
||||
s->wakeup = WAKEUP_EVENT_SOURCE;
|
||||
s->child.options = options;
|
||||
@@ -3264,7 +3290,6 @@ _public_ int sd_event_source_send_child_signal(sd_event_source *s, int sig, cons
|
||||
assert_return(s->type == SOURCE_CHILD, -EDOM);
|
||||
assert_return(!event_origin_changed(s->event), -ECHILD);
|
||||
assert_return(SIGNAL_VALID(sig), -EINVAL);
|
||||
assert(s->child.pidfd >= 0);
|
||||
|
||||
/* If we already have seen indication the process exited refuse sending a signal early. */
|
||||
if (s->child.exited)
|
||||
@@ -3276,14 +3301,20 @@ _public_ int sd_event_source_send_child_signal(sd_event_source *s, int sig, cons
|
||||
if (si)
|
||||
copy = *si;
|
||||
|
||||
return RET_NERRNO(pidfd_send_signal(s->child.pidfd, sig, si ? © : NULL, flags));
|
||||
if (s->child.pidfd >= 0)
|
||||
return RET_NERRNO(pidfd_send_signal(s->child.pidfd, sig, si ? © : NULL, flags));
|
||||
|
||||
if (flags != 0)
|
||||
return -EOPNOTSUPP;
|
||||
if (si)
|
||||
return RET_NERRNO(syscall(SYS_rt_sigqueueinfo, s->child.pid, sig, ©));
|
||||
return RET_NERRNO(kill(s->child.pid, sig));
|
||||
}
|
||||
|
||||
_public_ int sd_event_source_get_child_pidfd_own(sd_event_source *s) {
|
||||
assert_return(s, -EINVAL);
|
||||
assert_return(s->type == SOURCE_CHILD, -EDOM);
|
||||
assert_return(!event_origin_changed(s->event), -ECHILD);
|
||||
assert(s->child.pidfd >= 0);
|
||||
|
||||
return s->child.pidfd_owned;
|
||||
}
|
||||
@@ -3292,7 +3323,9 @@ _public_ int sd_event_source_set_child_pidfd_own(sd_event_source *s, int own) {
|
||||
assert_return(s, -EINVAL);
|
||||
assert_return(s->type == SOURCE_CHILD, -EDOM);
|
||||
assert_return(!event_origin_changed(s->event), -ECHILD);
|
||||
assert(s->child.pidfd >= 0);
|
||||
|
||||
if (s->child.pidfd < 0)
|
||||
return -EOPNOTSUPP;
|
||||
|
||||
s->child.pidfd_owned = own;
|
||||
return 0;
|
||||
@@ -3764,7 +3797,6 @@ static int process_child(sd_event *e, int64_t threshold, int64_t *ret_min_priori
|
||||
|
||||
HASHMAP_FOREACH(s, e->child_sources) {
|
||||
assert(s->type == SOURCE_CHILD);
|
||||
assert(s->child.pidfd >= 0);
|
||||
|
||||
if (s->priority > threshold)
|
||||
continue;
|
||||
@@ -3784,7 +3816,7 @@ static int process_child(sd_event *e, int64_t threshold, int64_t *ret_min_priori
|
||||
continue;
|
||||
|
||||
zero(s->child.siginfo);
|
||||
if (waitid(P_PIDFD, s->child.pidfd, &s->child.siginfo,
|
||||
if (child_waitid(s, &s->child.siginfo,
|
||||
WNOHANG | (s->child.options & WEXITED ? WNOWAIT : 0) | (s->child.options & ~WNOWAIT)) < 0)
|
||||
return negative_errno();
|
||||
|
||||
@@ -3798,7 +3830,7 @@ static int process_child(sd_event *e, int64_t threshold, int64_t *ret_min_priori
|
||||
* from the queue, since there's no benefit in leaving it queued. */
|
||||
|
||||
assert(s->child.options & (WSTOPPED|WCONTINUED));
|
||||
(void) waitid(P_PIDFD, s->child.pidfd, &s->child.siginfo, WNOHANG|(s->child.options & (WSTOPPED|WCONTINUED)));
|
||||
(void) child_waitid(s, &s->child.siginfo, WNOHANG|(s->child.options & (WSTOPPED|WCONTINUED)));
|
||||
}
|
||||
|
||||
r = source_set_pending(s, true);
|
||||
@@ -4281,7 +4313,7 @@ static int source_dispatch(sd_event_source *s) {
|
||||
|
||||
/* Now, reap the PID for good (unless WNOWAIT was specified by the caller). */
|
||||
if (zombie) {
|
||||
(void) waitid(P_PIDFD, s->child.pidfd, &s->child.siginfo, WNOHANG|WEXITED|(s->child.options & WNOWAIT));
|
||||
(void) child_waitid(s, &s->child.siginfo, WNOHANG|WEXITED|(s->child.options & WNOWAIT));
|
||||
if (!FLAGS_SET(s->child.options, WNOWAIT))
|
||||
s->child.waited = true;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,10 @@ static ssize_t fiber_io_operation(
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = sd_future_result(io);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return func(fd, args);
|
||||
}
|
||||
|
||||
@@ -227,9 +231,13 @@ int sd_fiber_connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = sd_fiber_await(io);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
/* future_new_io resolves with the revents mask on success; translate any positive value
|
||||
* (e.g. POLLOUT) back to the connect(2) success status. */
|
||||
r = sd_fiber_await(io);
|
||||
r = sd_future_result(io);
|
||||
return r > 0 ? 0 : r;
|
||||
}
|
||||
|
||||
@@ -445,6 +453,11 @@ int sd_fiber_ppoll(struct pollfd *fds, size_t n_fds, const struct timespec *time
|
||||
r = sd_fiber_await(group);
|
||||
if (r < 0 && r != -ETIME)
|
||||
return r;
|
||||
if (r >= 0) {
|
||||
r = sd_future_result(group);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
/* Always sweep fds with a non-blocking ppoll(): the timer and an fd readiness can resolve in
|
||||
* the same event-loop tick (or the fd can become ready between the timer firing and us being
|
||||
|
||||
@@ -822,7 +822,11 @@ int sd_fiber_sleep(uint64_t usec) {
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return sd_fiber_await(timer);
|
||||
r = sd_fiber_await(timer);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return sd_future_result(timer);
|
||||
}
|
||||
|
||||
static int fiber_wait_callback(sd_future *target, void *userdata) {
|
||||
@@ -861,7 +865,7 @@ int sd_fiber_await(sd_future *target) {
|
||||
assert_return(sd_future_get_event(target) == sd_future_get_event(f), -EINVAL);
|
||||
|
||||
if (sd_future_state(target) == SD_FUTURE_RESOLVED)
|
||||
return sd_future_result(target);
|
||||
return 0;
|
||||
|
||||
/* The fiber is executing inside event dispatch, so its loop cannot have finished yet.
|
||||
* Waiting during exit is supported by the slot's exit source. */
|
||||
@@ -879,8 +883,7 @@ int sd_fiber_await(sd_future *target) {
|
||||
if (sd_future_state(target) != SD_FUTURE_RESOLVED)
|
||||
return -EBUSY;
|
||||
|
||||
/* Read the result while the slot still pins the target: releasing it may drop the last ref. */
|
||||
return sd_future_result(target);
|
||||
return 0;
|
||||
}
|
||||
|
||||
sd_future* sd_future_cancel_wait_unref(sd_future *f) {
|
||||
|
||||
@@ -379,16 +379,17 @@ TEST(fiber_wait_for_completed) {
|
||||
ASSERT_OK_EQ(sd_future_result(target), 100);
|
||||
}
|
||||
|
||||
/* Test: awaiting an already-resolved future returns the future's result directly */
|
||||
/* Test: awaiting an already-resolved future completes immediately */
|
||||
static int await_resolved_fiber(void *userdata) {
|
||||
sd_future *target = userdata;
|
||||
|
||||
ASSERT_EQ((int) sd_future_state(target), (int) SD_FUTURE_RESOLVED);
|
||||
ASSERT_OK_EQ(sd_fiber_await(target), 77);
|
||||
ASSERT_OK_ZERO(sd_fiber_await(target));
|
||||
ASSERT_OK_EQ(sd_future_result(target), 77);
|
||||
return 0;
|
||||
}
|
||||
|
||||
TEST(fiber_await_resolved_returns_result) {
|
||||
TEST(fiber_await_resolved) {
|
||||
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
|
||||
ASSERT_OK(sd_event_new(&e));
|
||||
ASSERT_OK(sd_event_set_exit_on_idle(e, true));
|
||||
@@ -1667,13 +1668,14 @@ static int await_result_fiber(void *userdata) {
|
||||
_cleanup_(sd_future_unrefp) sd_future *awaited = NULL;
|
||||
|
||||
ASSERT_OK(sd_future_new_defer(sd_fiber_get_event(), result, &awaited));
|
||||
ASSERT_EQ(sd_fiber_await(awaited), result); /* Pending. */
|
||||
ASSERT_EQ(sd_fiber_await(awaited), result); /* Already resolved. */
|
||||
ASSERT_OK_ZERO(sd_fiber_await(awaited)); /* Pending. */
|
||||
ASSERT_EQ(sd_future_result(awaited), result);
|
||||
ASSERT_OK_ZERO(sd_fiber_await(awaited)); /* Already resolved. */
|
||||
ASSERT_OK_ZERO(sd_fiber_yield());
|
||||
|
||||
/* An already-resolved future must leave an unrelated pending interruption alone. */
|
||||
ASSERT_OK(sd_fiber_resume(sd_fiber_get_current(), -ECANCELED));
|
||||
ASSERT_EQ(sd_fiber_await(awaited), result);
|
||||
ASSERT_OK_ZERO(sd_fiber_await(awaited));
|
||||
ASSERT_ERROR(sd_fiber_yield(), ECANCELED);
|
||||
}
|
||||
|
||||
@@ -1693,7 +1695,14 @@ TEST(fiber_await_result) {
|
||||
}
|
||||
|
||||
static int await_borrowed_fiber(void *userdata) {
|
||||
return sd_fiber_await(userdata);
|
||||
_cleanup_(sd_future_unrefp) sd_future *target = sd_future_ref(ASSERT_PTR(userdata));
|
||||
int r;
|
||||
|
||||
r = sd_fiber_await(target);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return sd_future_result(target);
|
||||
}
|
||||
|
||||
TEST(fiber_await_last_reference) {
|
||||
@@ -1709,11 +1718,11 @@ TEST(fiber_await_last_reference) {
|
||||
ASSERT_OK(sd_fiber_new(e, "await-borrowed", await_borrowed_fiber, target,
|
||||
/* destroy= */ NULL, &waiter));
|
||||
|
||||
/* Suspend in await, so its non-floating callback slot holds a reference to target. */
|
||||
/* Suspend in await, so the waiter's own reference and the wait's slot keep target alive. */
|
||||
ASSERT_OK_POSITIVE(sd_event_run(e, 0));
|
||||
ASSERT_EQ(sd_future_state(waiter), SD_FUTURE_PENDING);
|
||||
|
||||
/* Leave the slot as the sole owner. Await must read the result before freeing it. */
|
||||
/* Drop our reference while the wait is in flight: the waiter still reads the result. */
|
||||
ASSERT_OK(sd_future_resolve(target, result));
|
||||
target = sd_future_unref(target);
|
||||
|
||||
|
||||
@@ -404,7 +404,7 @@ TEST(future_group_cancels_parent_on_child_error) {
|
||||
ASSERT_ERROR(s.group_result, EINVAL);
|
||||
}
|
||||
|
||||
/* Awaiting a group must return the child's error instead of cancelling the waiting parent. */
|
||||
/* Awaiting a group must complete with the child's error instead of cancelling the waiting parent. */
|
||||
static int await_gets_error_driver(void *userdata) {
|
||||
_cleanup_(sd_future_unrefp) sd_future *group = NULL, *errorer = NULL;
|
||||
int *await_result = ASSERT_PTR(userdata);
|
||||
@@ -413,7 +413,8 @@ static int await_gets_error_driver(void *userdata) {
|
||||
ASSERT_OK(sd_future_new_defer(sd_fiber_get_event(), -EINVAL, &errorer));
|
||||
ASSERT_OK(sd_future_group_add(group, errorer));
|
||||
|
||||
*await_result = sd_fiber_await(group);
|
||||
ASSERT_OK_ZERO(sd_fiber_await(group));
|
||||
*await_result = sd_future_result(group);
|
||||
ASSERT_OK_ZERO(sd_fiber_yield());
|
||||
return 0;
|
||||
}
|
||||
@@ -462,10 +463,9 @@ typedef struct AddResolvedState {
|
||||
static int add_resolved_driver(void *userdata) {
|
||||
AddResolvedState *s = ASSERT_PTR(userdata);
|
||||
|
||||
/* Drive the pre-built child to completion. The defer resolves with -EINVAL, which
|
||||
* makes sd_fiber_await return -EINVAL — we don't ASSERT_OK that. The child is now
|
||||
* RESOLVED and we can add it to the group. */
|
||||
(void) sd_fiber_await(s->child);
|
||||
/* Drive the pre-built child to completion. The defer resolves with -EINVAL, but the wait
|
||||
* itself succeeds. The child is now RESOLVED and we can add it to the group. */
|
||||
ASSERT_OK_ZERO(sd_fiber_await(s->child));
|
||||
ASSERT_EQ(sd_future_state(s->child), SD_FUTURE_RESOLVED);
|
||||
|
||||
s->add_result = sd_future_group_add(s->group, s->child);
|
||||
@@ -473,7 +473,8 @@ static int add_resolved_driver(void *userdata) {
|
||||
|
||||
/* Await drives the loop one more tick so the defer that wraps the RESOLVED child can
|
||||
* fire group_child_resolved and settle the group. */
|
||||
s->await_result = sd_fiber_await(s->group);
|
||||
ASSERT_OK_ZERO(sd_fiber_await(s->group));
|
||||
s->await_result = sd_future_result(s->group);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -510,7 +511,8 @@ typedef struct AddManyState {
|
||||
static int add_many_driver(void *userdata) {
|
||||
AddManyState *s = ASSERT_PTR(userdata);
|
||||
ASSERT_OK(sd_future_group_add_many(s->group, s->a, s->b, s->c));
|
||||
s->join_result = sd_fiber_await(s->group);
|
||||
ASSERT_OK_ZERO(sd_fiber_await(s->group));
|
||||
s->join_result = sd_future_result(s->group);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -744,12 +746,12 @@ static int parent_await_driver(void *userdata) {
|
||||
s->ready = true;
|
||||
ASSERT_EQ(sd_fiber_suspend(), s->result);
|
||||
|
||||
/* Parent cancellation does not replace the group's result. A later await of the resolved
|
||||
* group returns that result, just like sd_future_result(). */
|
||||
/* Parent cancellation does not replace the group's result, and a later await of the
|
||||
* resolved group completes immediately. */
|
||||
if (s->result == -ECANCELED) {
|
||||
ASSERT_EQ(sd_future_state(s->group), SD_FUTURE_RESOLVED);
|
||||
ASSERT_ERROR(sd_future_result(s->group), EIO);
|
||||
ASSERT_ERROR(sd_fiber_await(s->group), EIO);
|
||||
ASSERT_OK_ZERO(sd_fiber_await(s->group));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -830,7 +832,14 @@ TEST(future_group_external_cancel_leaves_parent_running) {
|
||||
}
|
||||
|
||||
static int peer_await_driver(void *userdata) {
|
||||
return sd_fiber_await(userdata);
|
||||
sd_future *group = ASSERT_PTR(userdata);
|
||||
int r;
|
||||
|
||||
r = sd_fiber_await(group);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return sd_future_result(group);
|
||||
}
|
||||
|
||||
TEST(future_group_peer_await_does_not_suppress_parent_cancel) {
|
||||
|
||||
@@ -117,7 +117,8 @@ int rtnl_resolve_ifname_full(
|
||||
r = sd_netlink_call(*rtnl, message, 0, &reply);
|
||||
if (r >= 0)
|
||||
return parse_newlink_message(reply, ret_name, ret_altnames);
|
||||
if (r != -ENODEV)
|
||||
/* The kernels older than 76c9ac0ee878f6693d398d3a95ccaf85e1f597a6 (v5.5) return -EINVAL. */
|
||||
if (!IN_SET(r, -ENODEV, -EINVAL))
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
@@ -866,7 +866,7 @@ static int create_session_message(
|
||||
|
||||
if (!avoid_pidfd) {
|
||||
pidfd = pidfd_open(getpid_cached(), 0);
|
||||
if (pidfd < 0)
|
||||
if (pidfd < 0 && !ERRNO_IS_NOT_SUPPORTED(errno)) /* ANCIENT-MOD: kernel < 5.3 */
|
||||
return -errno;
|
||||
}
|
||||
|
||||
|
||||
@@ -2448,6 +2448,38 @@ static int link_update_driver(Link *link, sd_netlink_message *message) {
|
||||
return 1; /* needs reconfigure */
|
||||
}
|
||||
|
||||
static int link_update_permanent_hardware_address_from_ethtool(Link *link, sd_netlink_message *message) {
|
||||
int r;
|
||||
|
||||
assert(link);
|
||||
assert(link->manager);
|
||||
assert(message);
|
||||
|
||||
if (link->ethtool_permanent_hw_addr_read)
|
||||
return 0;
|
||||
|
||||
/* When udevd is running, read the permanent hardware address after the interface is
|
||||
* initialized by udevd. Otherwise, ethtool may not work correctly. See issue #22538.
|
||||
* When udevd is not running, read the value when the interface is detected. */
|
||||
if (udev_available() && !link->dev)
|
||||
return 0;
|
||||
|
||||
/* If the interface does not have a hardware address, then it will not have a permanent address either. */
|
||||
r = netlink_message_read_hw_addr(message, IFLA_ADDRESS, NULL);
|
||||
if (r == -ENODATA)
|
||||
return 0;
|
||||
if (r < 0)
|
||||
return log_link_debug_errno(link, r, "Failed to read IFLA_ADDRESS attribute: %m");
|
||||
|
||||
link->ethtool_permanent_hw_addr_read = true;
|
||||
|
||||
r = ethtool_get_permanent_hw_addr(&link->manager->ethtool_fd, link->ifname, &link->permanent_hw_addr);
|
||||
if (r < 0)
|
||||
log_link_debug_errno(link, r, "Permanent hardware address not found, continuing without: %m");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int link_update_permanent_hardware_address(Link *link, sd_netlink_message *message) {
|
||||
int r;
|
||||
|
||||
@@ -2459,10 +2491,15 @@ static int link_update_permanent_hardware_address(Link *link, sd_netlink_message
|
||||
return 0;
|
||||
|
||||
r = netlink_message_read_hw_addr(message, IFLA_PERM_ADDRESS, &link->permanent_hw_addr);
|
||||
if (r == -ENODATA)
|
||||
return 0;
|
||||
if (r < 0)
|
||||
return log_link_debug_errno(link, r, "Failed to read IFLA_PERM_ADDRESS attribute: %m");
|
||||
if (r < 0) {
|
||||
if (r != -ENODATA)
|
||||
return log_link_debug_errno(link, r, "Failed to read IFLA_PERM_ADDRESS attribute: %m");
|
||||
|
||||
/* Fallback to ethtool for kernels older than v5.6 (f74877a5457d34d604dba6dbbb13c4c05bac8b93). */
|
||||
r = link_update_permanent_hardware_address_from_ethtool(link, message);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
if (link->permanent_hw_addr.length > 0)
|
||||
log_link_debug(link, "Saved permanent hardware address: %s", HW_ADDR_TO_STR(&link->permanent_hw_addr));
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
#include "networkd-wwan-bus.h"
|
||||
#include "ordered-set.h"
|
||||
#include "qdisc.h"
|
||||
#include "selinux-util.h"
|
||||
#include "set.h"
|
||||
#include "stat-util.h"
|
||||
#include "string-util.h"
|
||||
@@ -1149,8 +1150,12 @@ int manager_enumerate(Manager *m) {
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Could not enumerate neighbors: %m");
|
||||
|
||||
/* ANCIENT-MOD: NextHop support is added in kernel v5.3 (65ee00a9409f751188a8cdc0988167858eb4a536),
|
||||
* and older kernels return -EOPNOTSUPP, or -EINVAL if SELinux is enabled. */
|
||||
r = manager_enumerate_nexthop(m);
|
||||
if (r < 0)
|
||||
if (r == -EOPNOTSUPP || (r == -EINVAL && mac_selinux_enforcing()))
|
||||
log_debug_errno(r, "Could not enumerate nexthops, ignoring: %m");
|
||||
else if (r < 0)
|
||||
return log_error_errno(r, "Could not enumerate nexthops: %m");
|
||||
|
||||
r = manager_enumerate_routes(m);
|
||||
|
||||
@@ -29,6 +29,15 @@ executables += [
|
||||
'report-cgroup-server.c',
|
||||
),
|
||||
},
|
||||
libexec_template + {
|
||||
'name' : 'systemd-report-diskspace',
|
||||
'sources' : files(
|
||||
'report-diskspace.c',
|
||||
'report-diskspace-server.c',
|
||||
),
|
||||
'dependencies' : libmount_cflags,
|
||||
'conditions' : ['HAVE_LIBMOUNT'],
|
||||
},
|
||||
libexec_template + {
|
||||
'name' : 'systemd-report-files',
|
||||
'sources' : files(
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include "sd-json.h"
|
||||
#include "sd-varlink.h"
|
||||
|
||||
#include "build.h"
|
||||
#include "dlopen-note.h"
|
||||
#include "log.h"
|
||||
#include "main-func.h"
|
||||
#include "report-diskspace.h"
|
||||
#include "varlink-io.systemd.Metrics.h"
|
||||
#include "varlink-util.h"
|
||||
#include "verbs.h"
|
||||
|
||||
COMMAND(
|
||||
"systemd-report-diskspace\0",
|
||||
"Report disk space metrics.",
|
||||
);
|
||||
|
||||
static int vl_server(void) {
|
||||
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *vs = NULL;
|
||||
int r;
|
||||
|
||||
r = varlink_server_new(&vs, SD_VARLINK_SERVER_INHERIT_USERDATA, /* userdata= */ NULL);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to allocate Varlink server: %m");
|
||||
|
||||
r = sd_varlink_server_add_interface(vs, &vl_interface_io_systemd_Metrics);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to add Varlink interface: %m");
|
||||
|
||||
r = sd_varlink_server_bind_method_many(
|
||||
vs,
|
||||
"io.systemd.Metrics.List", vl_method_list_metrics,
|
||||
"io.systemd.Metrics.Describe", vl_method_describe_metrics);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to bind Varlink methods: %m");
|
||||
|
||||
r = sd_varlink_server_loop_auto(vs);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to run Varlink event loop: %m");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_argv(int argc, char *argv[]) {
|
||||
int r;
|
||||
|
||||
assert(argc >= 0);
|
||||
assert(argv);
|
||||
|
||||
OptionParser opts = { argc, argv };
|
||||
|
||||
FOREACH_OPTION_OR_RETURN(c, &opts)
|
||||
switch (c) {
|
||||
|
||||
OPTION_COMMON_HELP:
|
||||
return command_print_help();
|
||||
|
||||
OPTION_COMMON_VERSION:
|
||||
return version();
|
||||
|
||||
OPTION_COMMON_INTROSPECT_CLI:
|
||||
return introspect_cli(SD_JSON_FORMAT_OFF);
|
||||
}
|
||||
|
||||
if (option_parser_get_n_args(&opts) > 0)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
||||
"This program takes no arguments.");
|
||||
|
||||
r = sd_varlink_invocation(SD_VARLINK_ALLOW_ACCEPT);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m");
|
||||
if (r == 0)
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
||||
"This program can only run as a Varlink service.");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int run(int argc, char *argv[]) {
|
||||
int r;
|
||||
|
||||
LIBMOUNT_NOTE(required);
|
||||
|
||||
log_setup();
|
||||
|
||||
r = parse_argv(argc, argv);
|
||||
if (r <= 0)
|
||||
return r;
|
||||
|
||||
return vl_server();
|
||||
}
|
||||
|
||||
DEFINE_MAIN_FUNCTION(run);
|
||||
@@ -0,0 +1,366 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/statvfs.h>
|
||||
|
||||
#include "sd-device.h"
|
||||
#include "sd-json.h"
|
||||
#include "sd-varlink.h"
|
||||
|
||||
#include "alloc-util.h"
|
||||
#include "blockdev-util.h"
|
||||
#include "device-util.h"
|
||||
#include "devnum-util.h"
|
||||
#include "fd-util.h"
|
||||
#include "hash-funcs.h"
|
||||
#include "hashmap.h"
|
||||
#include "json-util.h"
|
||||
#include "libmount-util.h"
|
||||
#include "log.h"
|
||||
#include "metrics.h"
|
||||
#include "mountpoint-util.h"
|
||||
#include "path-util.h"
|
||||
#include "report-diskspace.h"
|
||||
#include "sort-util.h"
|
||||
#include "stat-util.h"
|
||||
|
||||
/* Reports the size of and the disk space still available on all mounted, writable, block device backed file
|
||||
* systems, i.e. roughly what df(1) shows. Pseudo file systems (procfs, sysfs, tmpfs, …), network file
|
||||
* systems and other file systems not backed by a block device are skipped, as are read-only mounts: their
|
||||
* free space is not going to change, and typically is not meaningful anyway (squashfs, erofs, DDIs, …).
|
||||
*
|
||||
* A file system (i.e. a superblock, identified by its device number) that is mounted in multiple places
|
||||
* (bind mounts, btrfs subvolumes, …) is reported only once, under its shortest mount path. */
|
||||
|
||||
typedef struct FileSystem {
|
||||
dev_t devno; /* the hashmap key: device number of the superblock */
|
||||
struct libmnt_fs *fs; /* the mount we report the file system under, owned by the mount table */
|
||||
int fd; /* O_PATH fd to the mount point */
|
||||
uint64_t free_bytes;
|
||||
uint64_t size_bytes;
|
||||
} FileSystem;
|
||||
|
||||
static FileSystem* file_system_free(FileSystem *f) {
|
||||
if (!f)
|
||||
return NULL;
|
||||
|
||||
safe_close(f->fd);
|
||||
return mfree(f);
|
||||
}
|
||||
|
||||
DEFINE_TRIVIAL_CLEANUP_FUNC(FileSystem*, file_system_free);
|
||||
DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
|
||||
file_system_hash_ops,
|
||||
dev_t, devt_hash_func, devt_compare_func,
|
||||
FileSystem, file_system_free);
|
||||
|
||||
static const char* file_system_path(const FileSystem *f) {
|
||||
assert(f);
|
||||
assert(f->fs);
|
||||
|
||||
return ASSERT_PTR(sym_mnt_fs_get_target(f->fs));
|
||||
}
|
||||
|
||||
/* Among the mount points of the same file system we prefer the shortest path, and among equally short ones
|
||||
* the alphabetically earlier. */
|
||||
static bool path_is_preferred(const char *a, const char *b) {
|
||||
assert(a);
|
||||
assert(b);
|
||||
|
||||
int ret = CMP(strlen(a), strlen(b));
|
||||
if (ret != 0)
|
||||
return ret < 0;
|
||||
|
||||
return path_compare(a, b) < 0;
|
||||
}
|
||||
|
||||
static int file_system_compare(FileSystem *const *a, FileSystem *const *b) {
|
||||
assert(a);
|
||||
assert(b);
|
||||
return path_compare(file_system_path(*a), file_system_path(*b));
|
||||
}
|
||||
|
||||
static int mount_collect(Hashmap **file_systems, struct libmnt_fs *fs) {
|
||||
int r;
|
||||
|
||||
assert(file_systems);
|
||||
assert(fs);
|
||||
|
||||
const char *target = sym_mnt_fs_get_target(fs);
|
||||
if (!target)
|
||||
return 0;
|
||||
|
||||
const char *fstype = sym_mnt_fs_get_fstype(fs);
|
||||
if (!fstype)
|
||||
return 0;
|
||||
|
||||
if (!fstype_is_blockdev_backed(fstype)) {
|
||||
log_debug("Mount '%s' of type %s is not backed by a block device, skipping.", target, fstype);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* This covers both file systems that are read-only as a whole, and read-only bind mounts of
|
||||
* otherwise writable file systems, since libmount searches both the superblock and the per-mount
|
||||
* options here. */
|
||||
r = sym_mnt_fs_get_option(fs, "ro", /* value= */ NULL, /* valsz= */ NULL);
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to check whether '%s' is mounted read-only: %m", target);
|
||||
if (r == 0) {
|
||||
log_debug("Mount '%s' is read-only, skipping.", target);
|
||||
return 0;
|
||||
}
|
||||
|
||||
dev_t devno = sym_mnt_fs_get_devno(fs);
|
||||
if (devno == 0) {
|
||||
log_debug("Mount '%s' has no device number, skipping.", target);
|
||||
return 0;
|
||||
}
|
||||
|
||||
FileSystem *existing = hashmap_get(*file_systems, &devno);
|
||||
if (existing && !path_is_preferred(target, file_system_path(existing))) {
|
||||
log_debug("File system mounted on '%s' is already known as '%s', skipping.",
|
||||
target, file_system_path(existing));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Mount points below a directory we lack access to (or that have been unmounted since the mount
|
||||
* table was read) are simply not considered. */
|
||||
_cleanup_close_ int fd = open(target, O_PATH|O_CLOEXEC);
|
||||
if (fd < 0) {
|
||||
log_debug_errno(errno, "Failed to open mount point '%s', ignoring: %m", target);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Make sure the path still refers to the mount we are looking at, and not to something that was
|
||||
* mounted on top of it since, shadowing it. Also catches mount points that are hidden entirely. */
|
||||
int mnt_id;
|
||||
r = path_get_mnt_id_at(fd, /* path= */ NULL, &mnt_id);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Failed to get mount ID of '%s', ignoring: %m", target);
|
||||
return 0;
|
||||
}
|
||||
if (mnt_id != sym_mnt_fs_get_id(fs)) {
|
||||
log_debug("Mount '%s' is shadowed by another mount, skipping.", target);
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct statfs sfs;
|
||||
r = xstatfsat(fd, /* path= */ NULL, &sfs);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Failed to statfs() '%s', ignoring: %m", target);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Already excluded above based on the mount table, but let's check again on the authoritative data,
|
||||
* in case things changed in the meantime. */
|
||||
if (FLAGS_SET(sfs.f_flags, ST_RDONLY)) {
|
||||
log_debug("Mount '%s' is read-only, skipping.", target);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (sfs.f_blocks == 0) {
|
||||
log_debug("Mount '%s' has zero size, skipping.", target);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Report the space available to unprivileged users as free, matching what df(1) shows as "Avail".
|
||||
* Space reserved for the superuser (typically 5% on ext4) is hence not counted as free. */
|
||||
uint64_t size_bytes, free_bytes;
|
||||
|
||||
if (!MUL_SAFE(&free_bytes, (uint64_t) sfs.f_frsize, (uint64_t) sfs.f_bavail) ||
|
||||
!MUL_SAFE(&size_bytes, (uint64_t) sfs.f_frsize, (uint64_t) sfs.f_blocks)) {
|
||||
log_debug("Sizes of '%s' overflow, skipping.", target);
|
||||
return 0;
|
||||
}
|
||||
|
||||
_cleanup_(file_system_freep) FileSystem *f = new(FileSystem, 1);
|
||||
if (!f)
|
||||
return log_oom();
|
||||
|
||||
*f = (FileSystem) {
|
||||
.devno = devno,
|
||||
.fs = fs,
|
||||
.fd = TAKE_FD(fd),
|
||||
.free_bytes = free_bytes,
|
||||
.size_bytes = size_bytes,
|
||||
};
|
||||
|
||||
/* Replace the entry we already have for this file system, if any, by the better one */
|
||||
file_system_free(hashmap_remove(*file_systems, &f->devno));
|
||||
|
||||
if (hashmap_ensure_put(file_systems, &file_system_hash_ops, &f->devno, f) < 0)
|
||||
return log_oom();
|
||||
|
||||
TAKE_PTR(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* If the file system's backing block device is a stacked one (LUKS-style DM, …), returns the device node of
|
||||
* the block device it originates from, chased down through all layers. Returns NULL (and success) if the
|
||||
* backing device is not a stacked block device, or is not a block device at all. */
|
||||
static int file_system_get_originating_source(const FileSystem *f, char **ret) {
|
||||
int r;
|
||||
|
||||
assert(f);
|
||||
assert(ret);
|
||||
|
||||
/* NB: We do not use f->devno here, but acquire the devno again from the fd. That's because on btrfs
|
||||
* we want the devnum of the backing device node, and not of the anonymous superblock */
|
||||
dev_t devno;
|
||||
r = get_block_device_fd(f->fd, &devno);
|
||||
if (r < 0) {
|
||||
return log_debug_errno(r, "Failed to get backing block device of '%s', ignoring: %m", file_system_path(f));
|
||||
*ret = NULL;
|
||||
return 0;
|
||||
}
|
||||
if (devno == 0) {
|
||||
log_debug("Path '%s' has no backing block device, ignoring.", file_system_path(f));
|
||||
*ret = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The mount's device number is the backing block device's (except for file systems that use
|
||||
* anonymous device numbers, such as btrfs, which we hence cannot trace this way). */
|
||||
_cleanup_(sd_device_unrefp) sd_device *dev = NULL;
|
||||
r = sd_device_new_from_devnum(&dev, 'b', devno);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Failed to get block device " DEVNUM_FORMAT_STR " backing '%s', ignoring: %m",
|
||||
DEVNUM_FORMAT_VAL(devno), file_system_path(f));
|
||||
*ret = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
_cleanup_(sd_device_unrefp) sd_device *origin = NULL;
|
||||
r = block_device_get_originating(dev, &origin, /* recursive= */ true);
|
||||
if (r < 0) {
|
||||
if (r != -ENOENT) /* -ENOENT means: not stacked */
|
||||
log_device_debug_errno(dev, r, "Failed to determine originating block device, ignoring: %m");
|
||||
*ret = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *node;
|
||||
r = sd_device_get_devname(origin, &node);
|
||||
if (r < 0) {
|
||||
log_device_debug_errno(origin, r, "Failed to get device node of originating block device, ignoring: %m");
|
||||
*ret = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return strdup_to(ret, node);
|
||||
}
|
||||
|
||||
static int file_system_send(
|
||||
const MetricFamily mf[static 2],
|
||||
sd_varlink *link,
|
||||
const FileSystem *f) {
|
||||
|
||||
int r;
|
||||
|
||||
assert(mf && mf[0].name && mf[1].name);
|
||||
assert(link);
|
||||
assert(f);
|
||||
|
||||
_cleanup_free_ char *originating_source = NULL;
|
||||
r = file_system_get_originating_source(f, &originating_source);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to determine originating source: %m");
|
||||
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *fields = NULL;
|
||||
r = sd_json_buildo(
|
||||
&fields,
|
||||
JSON_BUILD_PAIR_STRING_NON_EMPTY("source", sym_mnt_fs_get_source(f->fs)),
|
||||
JSON_BUILD_PAIR_STRING_NON_EMPTY("originatingSource", originating_source),
|
||||
SD_JSON_BUILD_PAIR_STRING("fileSystemType", sym_mnt_fs_get_fstype(f->fs)));
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to build metric fields: %m");
|
||||
|
||||
r = metric_build_send_unsigned(mf + 0, link, file_system_path(f), f->free_bytes, fields);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return metric_build_send_unsigned(mf + 1, link, file_system_path(f), f->size_bytes, fields);
|
||||
}
|
||||
|
||||
static int diskspace_generate(
|
||||
const MetricFamily mf[static 2],
|
||||
sd_varlink *link,
|
||||
void *userdata) {
|
||||
|
||||
int r;
|
||||
|
||||
assert(mf);
|
||||
assert(link);
|
||||
|
||||
_cleanup_(mnt_free_tablep) struct libmnt_table *table = NULL;
|
||||
_cleanup_(mnt_free_iterp) struct libmnt_iter *iter = NULL;
|
||||
r = libmount_parse_mountinfo(/* source= */ NULL, &table, &iter);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to parse /proc/self/mountinfo: %m");
|
||||
|
||||
/* First pass: collect all file systems we care about, keyed by superblock */
|
||||
_cleanup_hashmap_free_ Hashmap *file_systems = NULL;
|
||||
for (;;) {
|
||||
struct libmnt_fs *fs;
|
||||
|
||||
r = sym_mnt_table_next_fs(table, iter, &fs);
|
||||
if (r == 1)
|
||||
break;
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to get next entry from /proc/self/mountinfo: %m");
|
||||
|
||||
r = mount_collect(&file_systems, fs);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
/* Second pass: report them, sorted by mount path for stable output */
|
||||
_cleanup_free_ FileSystem **sorted = NULL;
|
||||
size_t n = 0;
|
||||
FileSystem *f;
|
||||
HASHMAP_FOREACH(f, file_systems) {
|
||||
if (!GREEDY_REALLOC(sorted, n + 1))
|
||||
return log_oom();
|
||||
|
||||
sorted[n++] = f;
|
||||
}
|
||||
|
||||
typesafe_qsort(sorted, n, file_system_compare);
|
||||
|
||||
FOREACH_ARRAY(i, sorted, n) {
|
||||
r = file_system_send(mf, link, *i);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const MetricFamily diskspace_metric_family_table[] = {
|
||||
/* Keep metrics ordered alphabetically. Both are generated by the same function, attached to the
|
||||
* first of them. */
|
||||
{
|
||||
"io.systemd.DiskSpace.FreeBytes",
|
||||
"Per file system metric: disk space available to unprivileged users in bytes, on writable "
|
||||
"block device backed file systems (object=mount point, fileSystemType=file system type, "
|
||||
"source=backing device, originatingSource=device the backing device is stacked on, if any)",
|
||||
METRIC_FAMILY_TYPE_GAUGE,
|
||||
.generate = diskspace_generate,
|
||||
},
|
||||
{
|
||||
"io.systemd.DiskSpace.SizeBytes",
|
||||
"Per file system metric: file system size in bytes, on writable block device backed file "
|
||||
"systems (object=mount point, fileSystemType=file system type, source=backing device, "
|
||||
"originatingSource=device the backing device is stacked on, if any)",
|
||||
METRIC_FAMILY_TYPE_GAUGE,
|
||||
},
|
||||
{}
|
||||
};
|
||||
|
||||
int vl_method_describe_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
|
||||
return metrics_method_describe(diskspace_metric_family_table, link, parameters, flags, userdata);
|
||||
}
|
||||
|
||||
int vl_method_list_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
|
||||
return metrics_method_list(diskspace_metric_family_table, link, parameters, flags, userdata);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
#pragma once
|
||||
|
||||
#include "forward.h"
|
||||
|
||||
int vl_method_list_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
|
||||
int vl_method_describe_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
|
||||
@@ -1024,6 +1024,7 @@ static int fw_nftables_add_local_dnat_internal(
|
||||
|
||||
sd_netlink_message *messages[3] = {};
|
||||
CLEANUP_ELEMENTS(messages, sd_netlink_message_unref_array_clear);
|
||||
static bool ipv6_supported = true;
|
||||
uint32_t data[5], key[2], dlen;
|
||||
size_t msgcnt = 0;
|
||||
int r;
|
||||
@@ -1032,6 +1033,9 @@ static int fw_nftables_add_local_dnat_internal(
|
||||
assert(add || !previous_remote);
|
||||
assert(IN_SET(af, AF_INET, AF_INET6));
|
||||
|
||||
if (!ipv6_supported && af == AF_INET6)
|
||||
return -EOPNOTSUPP;
|
||||
|
||||
if (!IN_SET(protocol, IPPROTO_TCP, IPPROTO_UDP))
|
||||
return -EPROTONOSUPPORT;
|
||||
|
||||
@@ -1085,6 +1089,14 @@ static int fw_nftables_add_local_dnat_internal(
|
||||
|
||||
assert(msgcnt < ELEMENTSOF(messages));
|
||||
r = sd_nfnl_call_batch(nfnl, messages, msgcnt, NFNL_DEFAULT_TIMEOUT_USECS);
|
||||
if (r == -EOVERFLOW && af == AF_INET6) {
|
||||
/* ANCIENT-MOD: The current implementation of DNAT in systemd requires kernel's
|
||||
* fdb9c405e35bdc6e305b9b4e20ebc141ed14fc81 (v5.8), and the older kernel returns
|
||||
* -EOVERFLOW. Let's treat the error as -EOPNOTSUPP. */
|
||||
log_debug_errno(r, "The current implementation of IPv6 DNAT in systemd requires kernel 5.8 or newer, ignoring: %m");
|
||||
ipv6_supported = false;
|
||||
return -EOPNOTSUPP;
|
||||
}
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
|
||||
@@ -64,6 +64,9 @@ static bool is_in_survivor_cgroup(const PidRef *pid) {
|
||||
}
|
||||
|
||||
r = cg_get_xattr_bool(cgroup_path, "user.survive_final_kill_signal");
|
||||
/* user xattr support was added to kernel v5.7, try with the trusted namespace as a fallback */
|
||||
if (ERRNO_IS_NEG_XATTR_ABSENT(r))
|
||||
r = cg_get_xattr_bool(cgroup_path, "trusted.survive_final_kill_signal");
|
||||
if (r < 0 && !ERRNO_IS_NEG_XATTR_ABSENT(r))
|
||||
log_debug_errno(r,
|
||||
"Failed to get survive_final_kill_signal xattr of %s, ignoring: %m",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
DLSYM_PROTOTYPE(mnt_free_iter) = NULL;
|
||||
DLSYM_PROTOTYPE(mnt_free_table) = NULL;
|
||||
DLSYM_PROTOTYPE(mnt_fs_get_devno) = NULL;
|
||||
DLSYM_PROTOTYPE(mnt_fs_get_fs_options) = NULL;
|
||||
DLSYM_PROTOTYPE(mnt_fs_get_fstype) = NULL;
|
||||
DLSYM_PROTOTYPE(mnt_fs_get_id) = NULL;
|
||||
@@ -154,6 +155,7 @@ int dlopen_libmount(int log_level) {
|
||||
log_level,
|
||||
DLSYM_ARG(mnt_free_iter),
|
||||
DLSYM_ARG(mnt_free_table),
|
||||
DLSYM_ARG(mnt_fs_get_devno),
|
||||
DLSYM_ARG(mnt_fs_get_fs_options),
|
||||
DLSYM_ARG(mnt_fs_get_fstype),
|
||||
DLSYM_ARG(mnt_fs_get_id),
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
extern DLSYM_PROTOTYPE(mnt_free_iter);
|
||||
extern DLSYM_PROTOTYPE(mnt_free_table);
|
||||
extern DLSYM_PROTOTYPE(mnt_fs_get_devno);
|
||||
extern DLSYM_PROTOTYPE(mnt_fs_get_fs_options);
|
||||
extern DLSYM_PROTOTYPE(mnt_fs_get_fstype);
|
||||
extern DLSYM_PROTOTYPE(mnt_fs_get_id);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#endif
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <linux/fs.h>
|
||||
#include <linux/loop.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/ioctl.h>
|
||||
@@ -200,6 +201,21 @@ static int loop_configure_fallback(int fd, const struct loop_config *c) {
|
||||
random_u64_range(UINT64_C(240) * USEC_PER_MSEC * n_attempts/64));
|
||||
}
|
||||
|
||||
/* Work around a kernel bug, where changing offset/size of the loopback device doesn't correctly
|
||||
* invalidate the buffer cache. For details see:
|
||||
*
|
||||
* https://android.googlesource.com/platform/system/apex/+/bef74542fbbb4cd629793f4efee8e0053b360570
|
||||
*
|
||||
* This was fixed in kernel 5.0, see:
|
||||
*
|
||||
* https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=5db470e229e22b7eda6e23b5566e532c96fb5bc3
|
||||
*
|
||||
* We'll run the work-around here in the legacy LOOP_SET_STATUS64 codepath. In the LOOP_CONFIGURE
|
||||
* codepath above it should not be necessary. */
|
||||
if (c->info.lo_offset != 0 || c->info.lo_sizelimit != 0)
|
||||
if (ioctl(fd, BLKFLSBUF, 0) < 0)
|
||||
log_debug_errno(errno, "Failed to issue BLKFLSBUF ioctl, ignoring: %m");
|
||||
|
||||
/* If a block size is requested then try to configure it. If that doesn't work, ignore errors, but
|
||||
* afterwards, let's validate what is in effect, and if it doesn't match what we want, fail */
|
||||
if (c->block_size != 0) {
|
||||
|
||||
+22
-13
@@ -43,26 +43,35 @@ typedef struct MountPoint {
|
||||
} MountPoint;
|
||||
|
||||
static int cgroupfs_mount_options(int priority, const char *type, char **ret) {
|
||||
_cleanup_free_ char *opts = NULL;
|
||||
int r;
|
||||
|
||||
assert(type);
|
||||
assert(streq(type, "cgroup2"));
|
||||
assert(ret);
|
||||
|
||||
/* memory_hugetlb_accounting mount option is since kernel v6.7 (8cba9576df601c384abd334a503c3f6e1e29eefb). */
|
||||
/* ANCIENT-MOD: memory_recursiveprot is probed (kernel >= 5.7) instead of being always passed.
|
||||
* memory_hugetlb_accounting mount option is since kernel v6.7 (8cba9576df601c384abd334a503c3f6e1e29eefb). */
|
||||
|
||||
r = mount_option_supported("cgroup2", "memory_hugetlb_accounting", /* value= */ NULL);
|
||||
if (r <= 0) {
|
||||
if (r < 0)
|
||||
log_full_errno(priority, r, "Failed to determine whether cgroupfs supports 'memory_hugetlb_accounting' mount option, assuming not: %m");
|
||||
else
|
||||
log_debug("'memory_hugetlb_accounting' not supported by cgroupfs, not using mount option.");
|
||||
|
||||
*ret = NULL;
|
||||
return 0;
|
||||
FOREACH_STRING(o, "memory_recursiveprot", "memory_hugetlb_accounting") {
|
||||
r = mount_option_supported("cgroup2", o, /* value= */ NULL);
|
||||
if (r == -EAGAIN)
|
||||
/* No fsopen(), i.e. kernel < 5.2, hence none of these options exist either */
|
||||
log_debug("Cannot probe cgroupfs mount option '%s', kernel too old, not using it.", o);
|
||||
else if (r < 0 && streq(o, "memory_recursiveprot")) {
|
||||
log_full_errno(priority, r, "Failed to determine whether cgroupfs supports '%s' mount option, assuming it does: %m", o);
|
||||
if (!strextend_with_separator(&opts, ",", o))
|
||||
return log_oom_full(priority);
|
||||
} else if (r < 0)
|
||||
log_full_errno(priority, r, "Failed to determine whether cgroupfs supports '%s' mount option, assuming not: %m", o);
|
||||
else if (r == 0)
|
||||
log_debug("'%s' not supported by cgroupfs, not using mount option.", o);
|
||||
else if (!strextend_with_separator(&opts, ",", o))
|
||||
return log_oom_full(priority);
|
||||
}
|
||||
|
||||
return strdup_to(ret, "memory_hugetlb_accounting");
|
||||
*ret = TAKE_PTR(opts);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mount_cgroupfs(const char *path) {
|
||||
@@ -82,7 +91,7 @@ int mount_cgroupfs(const char *path) {
|
||||
return r;
|
||||
|
||||
/* These options shall be kept in sync with those in mount_table below. */
|
||||
if (!strprepend_with_separator(&opts, ",", "nsdelegate,memory_recursiveprot"))
|
||||
if (!strprepend_with_separator(&opts, ",", "nsdelegate"))
|
||||
return log_oom();
|
||||
|
||||
return mount_nofollow_verbose(LOG_ERR, "cgroup2", path, "cgroup2", MS_NOSUID|MS_NOEXEC|MS_NODEV, opts);
|
||||
@@ -200,7 +209,7 @@ static const MountPoint mount_table[] = {
|
||||
.what = "cgroup2",
|
||||
.where = "/sys/fs/cgroup",
|
||||
.type = "cgroup2",
|
||||
.options = "nsdelegate,memory_recursiveprot",
|
||||
.options = "nsdelegate",
|
||||
.options_fn = cgroupfs_mount_options,
|
||||
.flags = MS_NOSUID|MS_NOEXEC|MS_NODEV,
|
||||
.mode = MNT_FATAL|MNT_IN_CONTAINER|MNT_CHECK_WRITABLE,
|
||||
|
||||
@@ -977,16 +977,14 @@ static int qmp_client_call_suspend(
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
/* An interruption takes precedence even if the reply arrived in the same tick: the wait
|
||||
* consumed the queued cancellation or timeout, and returning the reply instead would drop it. */
|
||||
r = sd_fiber_await(call);
|
||||
|
||||
/* If the future isn't resolved, the suspend was interrupted before a reply arrived (fiber
|
||||
* cancelled, fiber-wide SD_FIBER_TIMEOUT scope expired, …). There's no reply to extract,
|
||||
* so surface the resume error directly. When the future is resolved, future_get_qmp_reply()
|
||||
* already encodes success (1), QMP-level error (-EIO with the desc captured if asked for),
|
||||
* and no-reply (negative future result) — pass it through. */
|
||||
if (sd_future_state(call) != SD_FUTURE_RESOLVED)
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
/* future_get_qmp_reply() encodes success (1), QMP-level error (-EIO with the desc captured if
|
||||
* asked for), and no-reply (negative future result) — pass it through. */
|
||||
return future_get_qmp_reply(call, ret_result, ret_error_desc);
|
||||
}
|
||||
|
||||
|
||||
@@ -141,11 +141,12 @@ sd_future* sd_fiber_get_awaiting(sd_future *f);
|
||||
|
||||
int sd_fiber_yield(void);
|
||||
int sd_fiber_sleep(uint64_t usec);
|
||||
/* Return the target's result on completion, or a wait error if the calling fiber is interrupted or
|
||||
* the wait fails. An interrupted wait does not imply that the target has resolved; once it has,
|
||||
* sd_future_result() gives its outcome independently of the wait's return value. An already-resolved
|
||||
* target returns its result without consuming a pending interruption. The target must belong to the
|
||||
* calling fiber's event loop. */
|
||||
/* Suspend until the target has resolved. Returns 0 once it has, and a negative error only if the wait
|
||||
* itself did not complete: the calling fiber was interrupted (-ECANCELED, -ETIME), woken for an
|
||||
* unrelated reason (-EBUSY), or the wait could not be set up. An interrupted wait does not imply that
|
||||
* the target has resolved. The target's outcome is available through sd_future_result() afterwards;
|
||||
* the caller must hold its own reference to read it. An already-resolved target returns 0 without
|
||||
* consuming a pending interruption. The target must belong to the calling fiber's event loop. */
|
||||
int sd_fiber_await(sd_future *target);
|
||||
int sd_fiber_suspend(void);
|
||||
int sd_fiber_resume(sd_future *f, int result);
|
||||
|
||||
@@ -343,13 +343,19 @@ TEST(close_all_fds) {
|
||||
_exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
r = ASSERT_OK(pidref_safe_fork("(caf-nomalloc)", flags, NULL));
|
||||
ASSERT_OK(r = pidref_safe_fork("(caf-nomalloc)", flags, NULL));
|
||||
if (r == 0) {
|
||||
test_close_all_fds_inner(close_all_fds_without_malloc);
|
||||
_exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
r = ASSERT_OK(pidref_safe_fork("(caf-frugal)", flags, NULL));
|
||||
ASSERT_OK(r = pidref_safe_fork("(caf-proc)", flags, NULL));
|
||||
if (r == 0) {
|
||||
test_close_all_fds_inner(close_all_fds_by_proc);
|
||||
_exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
ASSERT_OK(r = pidref_safe_fork("(caf-frugal)", flags, NULL));
|
||||
if (r == 0) {
|
||||
test_close_all_fds_inner(close_all_fds_frugal);
|
||||
_exit(EXIT_SUCCESS);
|
||||
|
||||
@@ -419,7 +419,7 @@ static int qmp_client_call_fiber(void *userdata) {
|
||||
sd_json_variant *status = ASSERT_NOT_NULL(sd_json_variant_by_key(result, "status"));
|
||||
ASSERT_STREQ(sd_json_variant_string(status), "running");
|
||||
|
||||
/* QMP-level error: future resolves with -EIO, sd_fiber_await() returns -EIO, and
|
||||
/* QMP-level error: the wait completes, the future resolves with -EIO, and
|
||||
* future_get_qmp_reply() also returns -EIO (mirroring future_get_bus_reply()) — with the
|
||||
* detailed description captured via error_desc on top, and result left NULL. */
|
||||
f = sd_future_unref(f);
|
||||
@@ -427,7 +427,7 @@ static int qmp_client_call_fiber(void *userdata) {
|
||||
error_desc = mfree(error_desc);
|
||||
|
||||
ASSERT_OK(qmp_client_call_future(client, "stop", NULL, &f));
|
||||
ASSERT_ERROR(sd_fiber_await(f), EIO);
|
||||
ASSERT_OK_ZERO(sd_fiber_await(f));
|
||||
ASSERT_ERROR(sd_future_result(f), EIO);
|
||||
ASSERT_ERROR(future_get_qmp_reply(f, &result, &error_desc), EIO);
|
||||
|
||||
@@ -541,6 +541,7 @@ static int qmp_client_call_interrupted_fiber(void *userdata) {
|
||||
ASSERT_NOT_NULL(result);
|
||||
ASSERT_NULL(error_desc);
|
||||
ASSERT_OK_ZERO(sd_fiber_await(mock));
|
||||
ASSERT_OK_ZERO(sd_future_result(mock));
|
||||
ASSERT_OK_ZERO(sd_fiber_yield());
|
||||
ASSERT_OK(sd_event_exit(sd_fiber_get_event(), 0));
|
||||
return 0;
|
||||
|
||||
@@ -217,6 +217,38 @@ TEST(terminal_get_size_dsr) {
|
||||
log_notice("terminal size via ioctl: rows=%u columns=%u", ws.ws_row, ws.ws_col);
|
||||
}
|
||||
|
||||
TEST(terminal_fix_size_vc) {
|
||||
struct winsize before = {}, after = {};
|
||||
int r;
|
||||
|
||||
/* On a virtual console terminal_fix_size() must not query the terminal at all: it should report
|
||||
* success without touching the dimensions the kernel gave us. */
|
||||
|
||||
_cleanup_close_ int fd = open("/dev/tty1", O_RDWR|O_NOCTTY|O_CLOEXEC);
|
||||
if (fd < 0)
|
||||
return (void) log_notice_errno(errno, "Can't open /dev/tty1, skipping: %m");
|
||||
|
||||
_cleanup_free_ char *name = NULL;
|
||||
r = getttyname_harder(fd, &name);
|
||||
if (r < 0)
|
||||
return (void) log_notice_errno(r, "Can't get name of /dev/tty1, skipping: %m");
|
||||
if (!tty_is_vc_resolve(name))
|
||||
return (void) log_notice("/dev/tty1 is not a virtual console, skipping.");
|
||||
|
||||
if (ioctl(fd, TIOCGWINSZ, &before) < 0)
|
||||
return (void) log_notice_errno(errno, "Can't get terminal size via ioctl, skipping: %m");
|
||||
|
||||
usec_t n = now(CLOCK_MONOTONIC);
|
||||
r = terminal_fix_size(fd, fd);
|
||||
log_info("%s took %s", __func__+5,
|
||||
FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), n), USEC_PER_MSEC));
|
||||
|
||||
ASSERT_OK_ZERO(r);
|
||||
ASSERT_OK_ERRNO(ioctl(fd, TIOCGWINSZ, &after));
|
||||
ASSERT_EQ(before.ws_row, after.ws_row);
|
||||
ASSERT_EQ(before.ws_col, after.ws_col);
|
||||
}
|
||||
|
||||
TEST(terminal_fix_size) {
|
||||
int r;
|
||||
|
||||
|
||||
@@ -160,12 +160,19 @@ as root. The host must have coco enabled in firmware and in the kernel (for SEV-
|
||||
e.g. `kvm_amd.sev_snp=1`, with `/dev/sev` accessible). On a host without the requested
|
||||
technology, or when running in `mkosi vm` mode, the tests are skipped.
|
||||
|
||||
Inside each guest, `guest-test-runner.sh` runs the guest-side checks and reports
|
||||
their results back to the host over a vsock socket, while the guest's overall
|
||||
pass/fail is relayed separately through its exit status. The host side collects
|
||||
both to produce the subtest result.
|
||||
|
||||
```
|
||||
meson test
|
||||
└─ integration-test-wrapper.py
|
||||
└─ mkosi boot ──► systemd-nspawn container
|
||||
└─ TEST-94-COCO.sh
|
||||
└─ systemd-vmspawn --coco=<type> ──► confidential VM
|
||||
└─ TEST-94-COCO.*.sh
|
||||
└─ systemd-vmspawn --coco=<type> ──► confidential VM
|
||||
└─ guest-test-runner.sh
|
||||
```
|
||||
|
||||
## Running the integration tests without building systemd from source
|
||||
|
||||
@@ -109,6 +109,7 @@ INTROSPECTABLE=(
|
||||
systemd-report
|
||||
systemd-report-basic
|
||||
systemd-report-cgroup
|
||||
systemd-report-diskspace
|
||||
systemd-report-files
|
||||
systemd-report-hwmon
|
||||
systemd-report-sign-plain
|
||||
|
||||
@@ -93,6 +93,105 @@ fi
|
||||
"$REPORT" describe io.systemd.HWMon
|
||||
"$REPORT" metrics io.systemd.HWMon
|
||||
|
||||
# test io.systemd.DiskSpace Metrics
|
||||
|
||||
# Mount a scratch ext4 file system and check that it is reported with plausible numbers, that a tmpfs next to
|
||||
# it is not, that additional bind mounts of it do not result in additional rows but the shortest writable
|
||||
# mount path wins regardless of mount order, that it disappears from the report once remounted read-only,
|
||||
# and that a device mapper device stacked on top of it is reported with the loop device as originating
|
||||
# source.
|
||||
diskspace_mount_tests() {
|
||||
# Loop devices and device mapper are not available in containers
|
||||
if systemd-detect-virt --quiet --container; then
|
||||
echo "Skipping io.systemd.DiskSpace mount tests in container."
|
||||
return
|
||||
fi
|
||||
|
||||
DISKSPACE_WORK="$(mktemp -d /var/tmp/report-diskspace.XXXXXX)"
|
||||
diskspace_cleanup() {
|
||||
set +e
|
||||
umount "$DISKSPACE_WORK/tmpfs" "$DISKSPACE_WORK/a" "$DISKSPACE_WORK/b" "$DISKSPACE_WORK/longer" "$DISKSPACE_WORK/ext4" "$DISKSPACE_WORK/dm"
|
||||
dmsetup remove report-diskspace
|
||||
systemd-dissect --detach "$DISKSPACE_WORK/scratch.raw"
|
||||
rm -rf "$DISKSPACE_WORK"
|
||||
set -e
|
||||
}
|
||||
trap diskspace_cleanup EXIT
|
||||
mkdir "$DISKSPACE_WORK/ext4" "$DISKSPACE_WORK/a" "$DISKSPACE_WORK/b" "$DISKSPACE_WORK/longer" "$DISKSPACE_WORK/dm" "$DISKSPACE_WORK/tmpfs"
|
||||
truncate -s 64M "$DISKSPACE_WORK/scratch.raw"
|
||||
mkfs.ext4 -q "$DISKSPACE_WORK/scratch.raw"
|
||||
DISKSPACE_LOOP="$(systemd-dissect --attach --loop-ref=report-diskspace "$DISKSPACE_WORK/scratch.raw")"
|
||||
mount "$DISKSPACE_LOOP" "$DISKSPACE_WORK/ext4"
|
||||
# "a" sorts before "b" and is just as short, but is read-only, hence "b" must be picked
|
||||
mount --bind -o ro "$DISKSPACE_WORK/ext4" "$DISKSPACE_WORK/a"
|
||||
mount --bind "$DISKSPACE_WORK/ext4" "$DISKSPACE_WORK/b"
|
||||
# "longer" is writable too, but has a longer path, hence must not win even though it is mounted last
|
||||
mount --bind "$DISKSPACE_WORK/ext4" "$DISKSPACE_WORK/longer"
|
||||
mount -t tmpfs tmpfs "$DISKSPACE_WORK/tmpfs"
|
||||
dd if=/dev/urandom of="$DISKSPACE_WORK/ext4/fill" bs=1M count=8
|
||||
sync -f "$DISKSPACE_WORK/ext4"
|
||||
|
||||
diskspace_metrics="$(varlinkctl call --more /run/systemd/report/io.systemd.DiskSpace io.systemd.Metrics.List {} | tr -d '\036')"
|
||||
# Exactly one row per metric for the scratch file system, reported under the shortest writable mount path
|
||||
[[ "$(echo "$diskspace_metrics" | jq -r --arg s "$DISKSPACE_LOOP" 'select(.name == "io.systemd.DiskSpace.SizeBytes" and .fields.source == $s) | .object')" == "$DISKSPACE_WORK/b" ]]
|
||||
[[ "$(echo "$diskspace_metrics" | jq -r --arg s "$DISKSPACE_LOOP" 'select(.name == "io.systemd.DiskSpace.FreeBytes" and .fields.source == $s) | .object')" == "$DISKSPACE_WORK/b" ]]
|
||||
diskspace_size="$(echo "$diskspace_metrics" | jq -r --arg o "$DISKSPACE_WORK/b" 'select(.name == "io.systemd.DiskSpace.SizeBytes" and .object == $o) | .value')"
|
||||
diskspace_free="$(echo "$diskspace_metrics" | jq -r --arg o "$DISKSPACE_WORK/b" 'select(.name == "io.systemd.DiskSpace.FreeBytes" and .object == $o) | .value')"
|
||||
# The file system cannot be larger than the image, and at least the 8M we wrote must be missing from the
|
||||
# free space.
|
||||
[[ "$diskspace_size" -gt 0 ]]
|
||||
[[ "$diskspace_size" -le $((64 * 1024 * 1024)) ]]
|
||||
[[ "$diskspace_free" -gt 0 ]]
|
||||
[[ $((diskspace_free + 8 * 1024 * 1024)) -le "$diskspace_size" ]]
|
||||
# df(1) derives the same two numbers from statfs(), hence they must match exactly.
|
||||
read -r diskspace_df_size diskspace_df_avail < <(df -B1 --output=size,avail "$DISKSPACE_WORK/ext4" | sed 1d)
|
||||
[[ "$diskspace_size" == "$diskspace_df_size" ]]
|
||||
[[ "$diskspace_free" == "$diskspace_df_avail" ]]
|
||||
# Both rows must carry the file system type as field, and no originating source, as a loop device is not
|
||||
# stacked on anything
|
||||
[[ "$(echo "$diskspace_metrics" | jq -r --arg o "$DISKSPACE_WORK/b" 'select(.object == $o) | .fields.fileSystemType' | sort -u)" == "ext4" ]]
|
||||
[[ "$(echo "$diskspace_metrics" | jq -r --arg o "$DISKSPACE_WORK/b" 'select(.object == $o) | .fields | has("originatingSource") | tostring' | sort -u)" == "false" ]]
|
||||
# The tmpfs may not be reported
|
||||
(! echo "$diskspace_metrics" | jq -r '.object' | grep -Fx "$DISKSPACE_WORK/tmpfs" >/dev/null)
|
||||
|
||||
# Once remounted read-only the file system must no longer be reported, regardless of the mount path
|
||||
mount -o remount,ro "$DISKSPACE_WORK/ext4"
|
||||
diskspace_metrics="$(varlinkctl call --more --graceful=io.systemd.Metrics.NoSuchMetric /run/systemd/report/io.systemd.DiskSpace io.systemd.Metrics.List {} | tr -d '\036')"
|
||||
(! echo "$diskspace_metrics" | jq -r '.fields.source' | grep -Fx "$DISKSPACE_LOOP" >/dev/null)
|
||||
|
||||
# Now stack a (linear) device mapper device on top of the loop device, and mount the file system through
|
||||
# that: the DM device must be reported as source, and the loop device it sits on as originating source. The
|
||||
# loop device must be unmounted first, as DM needs exclusive access to it.
|
||||
umount "$DISKSPACE_WORK/a" "$DISKSPACE_WORK/b" "$DISKSPACE_WORK/longer" "$DISKSPACE_WORK/ext4"
|
||||
dmsetup create report-diskspace --table "0 $(blockdev --getsz "$DISKSPACE_LOOP") linear $DISKSPACE_LOOP 0"
|
||||
udevadm wait --timeout=30 --settle /dev/mapper/report-diskspace
|
||||
mount /dev/mapper/report-diskspace "$DISKSPACE_WORK/dm"
|
||||
diskspace_metrics="$(varlinkctl call --more /run/systemd/report/io.systemd.DiskSpace io.systemd.Metrics.List {} | tr -d '\036')"
|
||||
[[ "$(echo "$diskspace_metrics" | jq -r --arg o "$DISKSPACE_WORK/dm" 'select(.object == $o) | .fields.source' | sort -u)" == "/dev/mapper/report-diskspace" ]]
|
||||
[[ "$(echo "$diskspace_metrics" | jq -r --arg o "$DISKSPACE_WORK/dm" 'select(.object == $o) | .fields.originatingSource' | sort -u)" == "$DISKSPACE_LOOP" ]]
|
||||
|
||||
diskspace_cleanup
|
||||
trap - EXIT
|
||||
}
|
||||
|
||||
# The socket and service are only built if libmount is available
|
||||
if ! systemctl cat systemd-report-diskspace.socket &>/dev/null; then
|
||||
echo "systemd-report-diskspace.socket is not installed, skipping io.systemd.DiskSpace tests."
|
||||
else
|
||||
systemctl start systemd-report-diskspace.socket
|
||||
varlinkctl info /run/systemd/report/io.systemd.DiskSpace
|
||||
varlinkctl list-methods /run/systemd/report/io.systemd.DiskSpace
|
||||
# varlinkctl emits a JSON-SEQ stream for --more; strip the record separators so that plain jq can consume it
|
||||
diskspace_describe="$(varlinkctl call --more /run/systemd/report/io.systemd.DiskSpace io.systemd.Metrics.Describe {} | tr -d '\036')"
|
||||
echo "$diskspace_describe" | jq -r 'select(.name == "io.systemd.DiskSpace.FreeBytes") | .type' | grep -wx gauge >/dev/null
|
||||
echo "$diskspace_describe" | jq -r 'select(.name == "io.systemd.DiskSpace.SizeBytes") | .type' | grep -wx gauge >/dev/null
|
||||
|
||||
diskspace_mount_tests
|
||||
|
||||
"$REPORT" describe io.systemd.DiskSpace
|
||||
"$REPORT" metrics io.systemd.DiskSpace
|
||||
fi
|
||||
|
||||
# test io.systemd.Basic Metrics
|
||||
# ensure the socket is running, as some distros don't enable it by default
|
||||
systemctl start systemd-report-basic.socket
|
||||
|
||||
+21
-82
@@ -1,20 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# Launch a confidential VM with systemd-vmspawn --coco= and verify the guest reports the expected
|
||||
# confidential-virtualization type. Runs only on coco-capable hardware: the harness --coco=any flag gates
|
||||
# the run (skipped otherwise), binds /dev/kvm + the coco device(s) into this boot-mode container, and
|
||||
# passes the resolved technology via $COCO_TYPE.
|
||||
# Confidential Computing test.
|
||||
#
|
||||
# The confidential guest boots the same mkosi test image. A one-shot unit injected into the guest checks
|
||||
# systemd-detect-virt --cvm and reports the verdict via systemd's exit-status notification over vsock
|
||||
# (SuccessAction=exit): the guest PID1 sends EXIT_STATUS to vmspawn's vmm notify socket and vmspawn exits
|
||||
# with it.
|
||||
# These only run on coco hardware and if the test environment isn't already virtualized, see
|
||||
# the harness --coco= flag and the documentation in test/integration-tests/README.md.
|
||||
#
|
||||
# Subtests are split per platform and launch path to reduce the number of required boots.
|
||||
# Test code runs on both sides:
|
||||
# - host: test vmspawn by running confidential guests. TEST-94-COCO/lib.sh provides helpers.
|
||||
# - guest: test guest side component coco behavior. Assertions via TEST-94-COCO/guest-test{,-runner}.sh.
|
||||
|
||||
set -eux
|
||||
set -o pipefail
|
||||
|
||||
# shellcheck source=test/units/util.sh
|
||||
. "$(dirname "$0")"/util.sh
|
||||
# shellcheck source=test/units/test-control.sh
|
||||
. "$(dirname "$0")"/test-control.sh
|
||||
|
||||
if [[ -v ASAN_OPTIONS ]]; then
|
||||
echo "vmspawn launches QEMU which doesn't work under ASan, skipping" | tee --append /skipped
|
||||
@@ -31,83 +34,19 @@ if ! find_qemu_binary; then
|
||||
exit 77
|
||||
fi
|
||||
|
||||
# The integration-test-wrapper resolves the host's coco technology on the
|
||||
# bare-metal host (where /sys/module is readable) and passes it via $COCO_TYPE.
|
||||
# The harness binds the guest image artifacts read-only at /work/vm-images.
|
||||
export IMAGE_DIR=/work/vm-images
|
||||
if [[ ! -f "$IMAGE_DIR/image.raw" || ! -f "$IMAGE_DIR/image.vmlinuz" || ! -f "$IMAGE_DIR/image.initrd" ]]; then
|
||||
echo "image artifacts not found in $IMAGE_DIR, skipping" | tee --append /skipped
|
||||
exit 77
|
||||
fi
|
||||
|
||||
# The integration-test-wrapper resolves the host's coco technology (where /sys/module is readable) and
|
||||
# passes it via $COCO_TYPE.
|
||||
if [[ -z "${COCO_TYPE:-}" ]]; then
|
||||
echo "COCO_TYPE not set (run via the integration harness with --coco), skipping" | tee --append /skipped
|
||||
exit 77
|
||||
fi
|
||||
echo "Host coco type: $COCO_TYPE"
|
||||
|
||||
# The harness binds the guest image read-only at /work/vm-images (the 'vm-images' test option).
|
||||
IMAGE_DIR=/work/vm-images
|
||||
if [[ ! -f "$IMAGE_DIR/image.raw" || ! -f "$IMAGE_DIR/image.vmlinuz" || ! -f "$IMAGE_DIR/image.initrd" ]]; then
|
||||
echo "guest image not found in $IMAGE_DIR, skipping" | tee --append /skipped
|
||||
exit 77
|
||||
fi
|
||||
echo "Guest image directory: $IMAGE_DIR"
|
||||
|
||||
MACHINE="coco-guest-$$"
|
||||
WORKDIR="$(mktemp -d)"
|
||||
at_exit() {
|
||||
set +e
|
||||
machinectl terminate "$MACHINE" 2>/dev/null || :
|
||||
rm -rf "$WORKDIR"
|
||||
}
|
||||
trap at_exit EXIT INT TERM
|
||||
|
||||
# One-shot unit for the guest: verify systemd-detect-virt --cvm matches the type vmspawn launched.
|
||||
# Created via the systemd.extra-unit.* credential and pulled into the boot by a multi-user.target drop-in.
|
||||
GUEST_PASS=123
|
||||
GUEST_UNIT="$(cat <<'EOF'
|
||||
[Unit]
|
||||
Description=coco guest self-check
|
||||
After=basic.target
|
||||
SuccessAction=exit
|
||||
SuccessActionExitStatus=@GUEST_PASS@
|
||||
FailureAction=exit
|
||||
FailureActionExitStatus=1
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=bash -c 'test "$(systemd-detect-virt --cvm)" = "@COCO_TYPE@"'
|
||||
EOF
|
||||
)"
|
||||
GUEST_UNIT="${GUEST_UNIT//@COCO_TYPE@/$COCO_TYPE}"
|
||||
GUEST_UNIT="${GUEST_UNIT//@GUEST_PASS@/$GUEST_PASS}"
|
||||
|
||||
GUEST_DROPIN="$(cat <<'EOF'
|
||||
[Unit]
|
||||
Wants=coco-guest.service
|
||||
After=coco-guest.service
|
||||
EOF
|
||||
)"
|
||||
|
||||
# Launch the confidential guest and capture its console output.
|
||||
# Direct linux boot should work on all coco platforms (the only boot path supported by SNP).
|
||||
vmspawn_rc=0
|
||||
timeout -k 30 300 systemd-vmspawn \
|
||||
--machine="$MACHINE" \
|
||||
--coco="$COCO_TYPE" \
|
||||
--ram=1G \
|
||||
--ephemeral \
|
||||
--image="$IMAGE_DIR/image.raw" \
|
||||
--linux="$IMAGE_DIR/image.vmlinuz" \
|
||||
--initrd="$IMAGE_DIR/image.initrd" \
|
||||
--tpm=no \
|
||||
--console=read-only \
|
||||
--set-credential="systemd.extra-unit.coco-guest.service:$GUEST_UNIT" \
|
||||
--set-credential="systemd.unit-dropin.multi-user.target:$GUEST_DROPIN" \
|
||||
selinux=0 systemd.firstboot=no rw \
|
||||
2>&1 | tee "$WORKDIR/console.log" || vmspawn_rc="${PIPESTATUS[0]}"
|
||||
if [[ "$vmspawn_rc" -eq 124 || "$vmspawn_rc" -eq 137 ]]; then
|
||||
echo "systemd-vmspawn was killed by timeout (exit $vmspawn_rc)" >&2
|
||||
cat "$WORKDIR/console.log" >&2
|
||||
exit 1
|
||||
elif [[ "$vmspawn_rc" -ne "$GUEST_PASS" ]]; then
|
||||
echo "guest coco self-check did not pass: vmspawn exit $vmspawn_rc (expected $GUEST_PASS)" >&2
|
||||
cat "$WORKDIR/console.log" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Guest correctly reported confidential virtualization: $COCO_TYPE"
|
||||
|
||||
touch /testok
|
||||
run_subtests_and_exit
|
||||
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# SEV-SNP, direct kernel boot: launch confidential guests with --linux=/--initrd=.
|
||||
|
||||
set -eux
|
||||
set -o pipefail
|
||||
|
||||
# shellcheck source=test/units/TEST-94-COCO/lib.sh
|
||||
. "$(dirname "$0")"/TEST-94-COCO/lib.sh
|
||||
|
||||
if [[ "${COCO_TYPE:?}" != "sev-snp" ]]; then
|
||||
echo "host coco type is '$COCO_TYPE', not sev-snp, skipping"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
MACHINE="coco-snp-direct-$$"
|
||||
WORKDIR="$(mktemp -d)"
|
||||
mkdir -p "$WORKDIR/honest" "$WORKDIR/hostile"
|
||||
at_exit() {
|
||||
set +e
|
||||
jobs -p | xargs -r kill 2>/dev/null
|
||||
machinectl terminate "$MACHINE-honest" 2>/dev/null || :
|
||||
machinectl terminate "$MACHINE-hostile" 2>/dev/null || :
|
||||
rm -rf "$WORKDIR"
|
||||
}
|
||||
trap at_exit EXIT
|
||||
|
||||
# General test case for SNP direct boot, covers:
|
||||
#
|
||||
# - detect_virt: systemd-detect-virt --cvm detects SNP as the CVM type
|
||||
# - creds_vmspawn: credential transport by vmspawn is working (via initrd for SNP)
|
||||
# - creds_cmdline: credential transport via cmdline is working (cmdline is measured on SNP)
|
||||
vmspawn_boot_coco "$MACHINE-honest" "$COCO_TYPE" "$WORKDIR/honest" 'detect_virt creds_vmspawn creds_cmdline' \
|
||||
--image="$IMAGE_DIR/image.raw" \
|
||||
--linux="$IMAGE_DIR/image.vmlinuz" \
|
||||
--initrd="$IMAGE_DIR/image.initrd" \
|
||||
--set-credential="$COCO_CRED_TRUSTED_ID:$COCO_CRED_TRUSTED_VALUE" \
|
||||
selinux=0 systemd.firstboot=no rw \
|
||||
"systemd.set_credential=$COCO_CRED_CMDLINE_ID:$COCO_CRED_CMDLINE_VALUE"
|
||||
echo "SEV-SNP direct-boot guest correctly reported confidential virtualization and trusted credentials"
|
||||
|
||||
# Offensive test:
|
||||
#
|
||||
# - creds_hostile_*: ensure the guest isn't tricked into consuming untrusted/unmeasured credentials.
|
||||
QEMU_EXTRA=(
|
||||
-smbios "type=11,value=io.systemd.credential:$COCO_CRED_SMBIOS_ID=$COCO_CRED_SMBIOS_VALUE"
|
||||
-smbios "type=11,value=io.systemd.credential:$COCO_CRED_TRUSTED_ID=$COCO_CRED_SMBIOS_VALUE"
|
||||
-fw_cfg "name=opt/io.systemd.credentials/$COCO_CRED_FWCFG_ID,string=$COCO_CRED_FWCFG_VALUE"
|
||||
)
|
||||
|
||||
SYSTEMD_VMSPAWN_QEMU_EXTRA="${QEMU_EXTRA[*]}" \
|
||||
vmspawn_boot_coco "$MACHINE-hostile" "$COCO_TYPE" "$WORKDIR/hostile" \
|
||||
'detect_virt creds_vmspawn creds_hostile_fwcfg creds_hostile_snp_smbios creds_hostile_shadow' \
|
||||
--image="$IMAGE_DIR/image.raw" \
|
||||
--linux="$IMAGE_DIR/image.vmlinuz" \
|
||||
--initrd="$IMAGE_DIR/image.initrd" \
|
||||
--set-credential="$COCO_CRED_TRUSTED_ID:$COCO_CRED_TRUSTED_VALUE" \
|
||||
selinux=0 systemd.firstboot=no rw
|
||||
echo "SEV-SNP direct-boot guest correctly ignored the host-injected credentials"
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# TDX, direct kernel boot: launch a confidential guest with --linux=/--initrd=.
|
||||
|
||||
set -eux
|
||||
set -o pipefail
|
||||
|
||||
# shellcheck source=test/units/TEST-94-COCO/lib.sh
|
||||
. "$(dirname "$0")"/TEST-94-COCO/lib.sh
|
||||
|
||||
if [[ "${COCO_TYPE:?}" != "tdx" ]]; then
|
||||
echo "host coco type is '$COCO_TYPE', not tdx, skipping"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
MACHINE="coco-tdx-direct-$$"
|
||||
WORKDIR="$(mktemp -d)"
|
||||
at_exit() {
|
||||
set +e
|
||||
jobs -p | xargs -r kill 2>/dev/null
|
||||
machinectl terminate "$MACHINE" 2>/dev/null || :
|
||||
rm -rf "$WORKDIR"
|
||||
}
|
||||
trap at_exit EXIT
|
||||
|
||||
vmspawn_boot_coco "$MACHINE" "$COCO_TYPE" "$WORKDIR" 'detect_virt' \
|
||||
--image="$IMAGE_DIR/image.raw" \
|
||||
--linux="$IMAGE_DIR/image.vmlinuz" \
|
||||
--initrd="$IMAGE_DIR/image.initrd" \
|
||||
selinux=0 systemd.firstboot=no rw
|
||||
echo "TDX direct-boot guest correctly reported confidential virtualization"
|
||||
@@ -0,0 +1,26 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# shellcheck shell=bash
|
||||
# Every constant here is consumed by whichever side sources this file, never by this file itself.
|
||||
# shellcheck disable=SC2034
|
||||
#
|
||||
# Fixtures for the confidential computing integration test (TEST-94-COCO). Shared by the host-side
|
||||
# boot scenarios, which deliver these credentials over trusted and untrusted channels alike, and by
|
||||
# the guest-side checks, which assert where each one ended up, so the two sides cannot drift apart.
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" -ef "$0" ]]; then
|
||||
echo >&2 "This file should not be executed directly"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Name of the guest self-check unit lib.sh injects.
|
||||
COCO_GUEST_UNIT="coco-guest.service"
|
||||
|
||||
# Credentials by channel
|
||||
COCO_CRED_TRUSTED_ID="coco.trusted"
|
||||
COCO_CRED_TRUSTED_VALUE="trusted-via-vmspawn"
|
||||
COCO_CRED_CMDLINE_ID="coco.cmdline"
|
||||
COCO_CRED_CMDLINE_VALUE="trusted-via-cmdline"
|
||||
COCO_CRED_SMBIOS_ID="coco.hostile.smbios"
|
||||
COCO_CRED_SMBIOS_VALUE="injected-via-smbios"
|
||||
COCO_CRED_FWCFG_ID="coco.hostile.fwcfg"
|
||||
COCO_CRED_FWCFG_VALUE="injected-via-fwcfg"
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# Runs inside the confidential guest launched by TEST-94-COCO.sh. Runs the requested guest-side checks
|
||||
# (testcase_coco_* from guest-test.sh), ships a per-check result record to the host over the vsock
|
||||
# result socket, and lets the injected unit's SuccessAction/FailureAction relay the aggregate verdict
|
||||
# to the host via vmspawn's exit status.
|
||||
set -eux
|
||||
set -o pipefail
|
||||
|
||||
_COCO_DIR="$(dirname "$(readlink -f "$0")")"
|
||||
# util.sh (assert_*) lives one level up in the shared units dir.
|
||||
# shellcheck source=test/units/util.sh
|
||||
. "$(dirname "$_COCO_DIR")/util.sh"
|
||||
# shellcheck source=test/units/TEST-94-COCO/guest-test.sh
|
||||
. "$_COCO_DIR/guest-test.sh"
|
||||
|
||||
# Ship the accumulated result records to the host's vsock listener (CID 2 = host). Best-effort: the
|
||||
# aggregate verdict travels independently via the unit's exit status, so a vsock hiccup never masks a
|
||||
# pass/fail — it only drops the per-check breakdown.
|
||||
_coco_ship_results() {
|
||||
local records="${1:?}"
|
||||
[[ -n "${COCO_RESULT_PORT:-}" ]] || return 0
|
||||
socat -u "OPEN:$records,rdonly" "VSOCK-CONNECT:2:$COCO_RESULT_PORT" ||
|
||||
echo >&2 "WARNING: failed to ship coco results over vsock port $COCO_RESULT_PORT"
|
||||
}
|
||||
|
||||
# Run the guest-side checks named by $COCO_TESTCASES (testcase_coco_* names without the prefix),
|
||||
# record pass/fail per check, and ship the records. Each named check is something its boot scenario
|
||||
# exists to prove, so an unknown name or an empty request fails the aggregate rather than letting the
|
||||
# boot pass without having verified anything.
|
||||
_coco_run_tests() {
|
||||
local testcases testcase rc result failed=0 records
|
||||
records="$(mktemp)"
|
||||
|
||||
read -ra testcases <<<"${COCO_TESTCASES:-}"
|
||||
if [[ "${#testcases[@]}" -eq 0 ]]; then
|
||||
echo >&2 "No coco testcases requested via \$COCO_TESTCASES"
|
||||
return 1
|
||||
fi
|
||||
testcases=("${testcases[@]/#/testcase_coco_}")
|
||||
|
||||
for testcase in "${testcases[@]}"; do
|
||||
if ! declare -F "$testcase" >/dev/null; then
|
||||
echo >&2 "Requested coco testcase '$testcase' is not defined"
|
||||
printf 'ID=%s RESULT=missing\n' "$testcase" >>"$records"
|
||||
failed=1
|
||||
continue
|
||||
fi
|
||||
# Subshell so a testcase's RETURN trap can't fire twice (see run_testcases in test-control.sh).
|
||||
# Bash disables 'set -e' throughout a function called in a context that tests its exit status,
|
||||
# so '("$testcase") || rc=$?' would let a testcase run past its first failed assertion and
|
||||
# report only its last one. Reading $? from a standalone call avoids that context; the inner
|
||||
# 'set -e' re-arms what the surrounding 'set +e' turned off.
|
||||
set +e
|
||||
(set -e; "$testcase")
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ "$rc" -eq 0 ]]; then
|
||||
result=pass
|
||||
else
|
||||
result=fail
|
||||
failed=1
|
||||
fi
|
||||
echo "coco check $testcase: $result (rc=$rc)"
|
||||
printf 'ID=%s RESULT=%s\n' "$testcase" "$result" >>"$records"
|
||||
done
|
||||
|
||||
_coco_ship_results "$records"
|
||||
return "$failed"
|
||||
}
|
||||
|
||||
_coco_run_tests
|
||||
@@ -0,0 +1,96 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Guest-side checks for the confidential computing integration test (TEST-94-COCO). Sourced by
|
||||
# guest-test-runner.sh inside the confidential guest; each testcase_coco_* function is one
|
||||
# sub-subtest, and a boot scenario names the checks it requires via $COCO_TESTCASES.
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" -ef "$0" ]]; then
|
||||
echo >&2 "This file should not be executed directly"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck source=test/units/TEST-94-COCO/fixtures.sh
|
||||
. "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/fixtures.sh"
|
||||
|
||||
# _coco_assert_cred_absent ID
|
||||
# Assert no credential named ID reached the guest at all, neither trusted nor encrypted.
|
||||
_coco_assert_cred_absent() {
|
||||
local id="${1:?}"
|
||||
local system_creds_dir=/run/credentials/@system
|
||||
local encrypted_creds_dir=/run/credentials/@encrypted
|
||||
|
||||
assert_fail test -e "$system_creds_dir/$id"
|
||||
assert_fail test -e "$encrypted_creds_dir/$id"
|
||||
}
|
||||
|
||||
# _coco_assert_smbios_injected ID
|
||||
# Assert the guest received an SMBIOS type #11 OEM string carrying credential ID. The scenario
|
||||
# injected it, so a miss means the injection was lost on the way in and concluding anything from the
|
||||
# credential's absence afterwards would be meaningless.
|
||||
_coco_assert_smbios_injected() {
|
||||
local id="${1:?}" raw
|
||||
|
||||
[[ -d /sys/firmware/dmi/entries ]] || modprobe dmi-sysfs || :
|
||||
|
||||
for raw in /sys/firmware/dmi/entries/11-*/raw; do
|
||||
[[ -e "$raw" ]] || continue
|
||||
# The entry is a blob of NUL-separated strings: --text to search it anyway, --fixed-strings
|
||||
# because credential names contain dots. Redirect rather than -q, which would exit early and
|
||||
# risk SIGPIPE on the reader.
|
||||
if grep --text --fixed-strings "io.systemd.credential:$id=" "$raw" >/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
echo "FAIL: no SMBIOS OEM string for credential '$id' in the guest's DMI type #11 tables" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# Assert the guest observes the confidential-virtualization technology the host launched it with.
|
||||
# COCO_TYPE is provided by the injected guest unit (Environment=, see lib.sh).
|
||||
testcase_coco_detect_virt() {
|
||||
assert_eq "$(systemd-detect-virt --cvm)" "${COCO_TYPE:?}"
|
||||
}
|
||||
|
||||
# Assert a credential handed to systemd-vmspawn --set-credential lands in the trusted bucket.
|
||||
#
|
||||
# SNP: creds should be delivered via the initrd cpio channel.
|
||||
testcase_coco_creds_vmspawn() {
|
||||
assert_eq "$(systemd-creds --system cat "$COCO_CRED_TRUSTED_ID")" "$COCO_CRED_TRUSTED_VALUE"
|
||||
assert_not_in "$COCO_CRED_TRUSTED_ID" "$(</proc/cmdline)"
|
||||
}
|
||||
|
||||
# Assert a credential passed on the kernel command line lands in the trusted bucket.
|
||||
#
|
||||
# Credentials passed on the cmdline should be accepted on every coco platform.
|
||||
testcase_coco_creds_cmdline() {
|
||||
assert_in "systemd.set_credential=$COCO_CRED_CMDLINE_ID:$COCO_CRED_CMDLINE_VALUE" "$(</proc/cmdline)"
|
||||
assert_eq "$(systemd-creds --system cat "$COCO_CRED_CMDLINE_ID")" "$COCO_CRED_CMDLINE_VALUE"
|
||||
}
|
||||
|
||||
# fw_cfg is unmeasured on every platform and import_credentials_qemu() drops it for any confidential
|
||||
# guest, so this applies to SNP and TDX alike.
|
||||
testcase_coco_creds_hostile_fwcfg() {
|
||||
local fwcfg_dir=/sys/firmware/qemu_fw_cfg/by_name
|
||||
|
||||
[[ -d "$fwcfg_dir" ]] || modprobe qemu_fw_cfg || :
|
||||
|
||||
assert_ok test -e "$fwcfg_dir/opt/io.systemd.credentials/$COCO_CRED_FWCFG_ID/raw"
|
||||
_coco_assert_cred_absent "$COCO_CRED_FWCFG_ID"
|
||||
}
|
||||
|
||||
# SNP only: the SMBIOS tables are not covered by the launch measurement, so
|
||||
# import_credentials_smbios() bails out. Under TDX they are measured into RTMR0 and thus trusted.
|
||||
testcase_coco_creds_hostile_snp_smbios() {
|
||||
_coco_assert_smbios_injected "$COCO_CRED_SMBIOS_ID"
|
||||
_coco_assert_cred_absent "$COCO_CRED_SMBIOS_ID"
|
||||
}
|
||||
|
||||
# A host-injected SMBIOS string must not displace a credential the measured channel already
|
||||
# delivered: boot-channel credentials import first and names are never overwritten, on every
|
||||
# platform. The probe proves the overwrite was attempted.
|
||||
testcase_coco_creds_hostile_shadow() {
|
||||
_coco_assert_smbios_injected "$COCO_CRED_TRUSTED_ID"
|
||||
assert_eq "$(systemd-creds --system cat "$COCO_CRED_TRUSTED_ID")" "$COCO_CRED_TRUSTED_VALUE"
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Host-side helpers for the confidential computing integration test (TEST-94-COCO).
|
||||
# Sourced by the TEST-94-COCO.sh driver.
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" -ef "$0" ]]; then
|
||||
echo >&2 "This file should not be executed directly"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck source=test/units/TEST-94-COCO/fixtures.sh
|
||||
. "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/fixtures.sh"
|
||||
|
||||
# Exit status the guest test runner reports on success (its unit's SuccessActionExitStatus=). vmspawn
|
||||
# forwards the guest PID1's EXIT_STATUS over the vsock notify socket and exits with it.
|
||||
_COCO_GUEST_PASS=123
|
||||
# vsock port the host listens on for the guest's per-check result records, distinct from vmspawn's own
|
||||
# (kernel-assigned) notify port. The host binds CID_ANY:PORT; the guest connects to CID 2 (host):PORT.
|
||||
_COCO_RESULT_PORT=4321
|
||||
|
||||
# _coco_guest_unit COCO_TYPE UNITS_DIR TESTCASES
|
||||
# Emit the one-shot unit that runs the guest test runner: passes the coco type, result port and
|
||||
# the requested checks via the environment, and relays the aggregate verdict via
|
||||
# SuccessAction/EXIT_STATUS.
|
||||
_coco_guest_unit() {
|
||||
local coco_type="${1:?}" units_dir="${2:?}" testcases="${3:?}"
|
||||
cat <<EOF
|
||||
[Unit]
|
||||
Description=coco guest self-check
|
||||
After=basic.target
|
||||
SuccessAction=exit
|
||||
SuccessActionExitStatus=$_COCO_GUEST_PASS
|
||||
FailureAction=exit
|
||||
FailureActionExitStatus=1
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Environment=COCO_TYPE=$coco_type
|
||||
Environment=COCO_RESULT_PORT=$_COCO_RESULT_PORT
|
||||
Environment="COCO_TESTCASES=$testcases"
|
||||
ExecStart=$units_dir/guest-test-runner.sh
|
||||
EOF
|
||||
}
|
||||
|
||||
# _coco_guest_dropin
|
||||
# Emit the multi-user.target drop-in that pulls the guest self-check unit into the boot.
|
||||
_coco_guest_dropin() {
|
||||
cat <<EOF
|
||||
[Unit]
|
||||
Wants=$COCO_GUEST_UNIT
|
||||
After=$COCO_GUEST_UNIT
|
||||
EOF
|
||||
}
|
||||
|
||||
# vmspawn_boot_coco MACHINE COCO_TYPE WORKDIR TESTCASES [systemd-vmspawn args...]
|
||||
# Boot a confidential guest with systemd-vmspawn, running the coco guest test runner. TESTCASES is the
|
||||
# space-separated list of guest checks (testcase_coco_* names without the prefix).
|
||||
# The caller supplies the boot-path-specific launch args — --image/--linux/--initrd and the guest
|
||||
# kernel command line — plus any extra flags via "$@", so each per-boot-path scenario
|
||||
# stays configurable. Per-check records shipped by the guest are collected over a vsock
|
||||
# socket into WORKDIR/results and echoed; returns 0 iff the guest reported the success
|
||||
# aggregate, otherwise dumps the console.
|
||||
vmspawn_boot_coco() {
|
||||
local machine="${1:?}" coco_type="${2:?}" workdir="${3:?}" testcases="${4:?}"
|
||||
shift 4
|
||||
|
||||
local units_dir results console guest_unit guest_dropin rc=0 listener_pid=""
|
||||
units_dir="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
|
||||
results="$workdir/results"
|
||||
console="$workdir/console.log"
|
||||
: >"$results"
|
||||
|
||||
guest_unit="$(_coco_guest_unit "$coco_type" "$units_dir" "$testcases")"
|
||||
guest_dropin="$(_coco_guest_dropin)"
|
||||
|
||||
# Collect the per-check records the guest ships over vsock (single connection, guest closes when
|
||||
# done). Best-effort: the aggregate exit status below is the authoritative pass/fail, so a vsock
|
||||
# hiccup only costs the per-check breakdown, never the verdict.
|
||||
socat -u "VSOCK-LISTEN:$_COCO_RESULT_PORT" "OPEN:$results,creat" &
|
||||
listener_pid=$!
|
||||
|
||||
timeout -k 30 300 systemd-vmspawn \
|
||||
--machine="$machine" \
|
||||
--coco="$coco_type" \
|
||||
--ram=1G \
|
||||
--ephemeral \
|
||||
--tpm=no \
|
||||
--console=read-only \
|
||||
--set-credential="systemd.extra-unit.$COCO_GUEST_UNIT:$guest_unit" \
|
||||
--set-credential="systemd.unit-dropin.multi-user.target:$guest_dropin" \
|
||||
"$@" \
|
||||
2>&1 | tee "$console" || rc="${PIPESTATUS[0]}"
|
||||
|
||||
kill "$listener_pid" 2>/dev/null || :
|
||||
wait "$listener_pid" 2>/dev/null || :
|
||||
|
||||
if [[ -s "$results" ]]; then
|
||||
echo "coco guest per-check results:"
|
||||
cat "$results"
|
||||
fi
|
||||
|
||||
if [[ "$rc" -eq 124 || "$rc" -eq 137 ]]; then
|
||||
echo "systemd-vmspawn was killed by timeout (exit $rc)" >&2
|
||||
cat "$console" >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ "$rc" -ne "$_COCO_GUEST_PASS" ]]; then
|
||||
echo "coco guest test runner failed: vmspawn exit $rc (expected $_COCO_GUEST_PASS)" >&2
|
||||
cat "$console" >&2
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
@@ -84,6 +84,8 @@ _show_summary() {(
|
||||
_record_subtest_rc() {
|
||||
local subtest="${1:?}" rc="${2:?}"
|
||||
|
||||
subtest="${subtest##*/}"
|
||||
|
||||
if [[ $rc -eq $_SUBTEST_SKIP_RC ]]; then
|
||||
echo "Subtest $subtest skipped"
|
||||
_SKIPPED_TESTS+=("$subtest")
|
||||
|
||||
@@ -588,3 +588,8 @@ systemd.network.xml ./refsect1[title="[ModemManager] Section Options"]/variablel
|
||||
systemd-report-hwmon@.service.xml ./refsect1[title="Description"]/variablelist/varlistentry[term="chip"]
|
||||
systemd-report-hwmon@.service.xml ./refsect1[title="Description"]/variablelist/varlistentry[term="sensor"]
|
||||
systemd-report-hwmon@.service.xml ./refsect1[title="Description"]/variablelist/varlistentry[term="label"]
|
||||
systemd-report-diskspace@.service.xml ./refsect1[title="Description"]/variablelist[1]/varlistentry[term="io.systemd.DiskSpace.SizeBytes"]
|
||||
systemd-report-diskspace@.service.xml ./refsect1[title="Description"]/variablelist[1]/varlistentry[term="io.systemd.DiskSpace.FreeBytes"]
|
||||
systemd-report-diskspace@.service.xml ./refsect1[title="Description"]/variablelist[2]/varlistentry[term="source"]
|
||||
systemd-report-diskspace@.service.xml ./refsect1[title="Description"]/variablelist[2]/varlistentry[term="originatingSource"]
|
||||
systemd-report-diskspace@.service.xml ./refsect1[title="Description"]/variablelist[2]/varlistentry[term="fileSystemType"]
|
||||
|
||||
@@ -794,6 +794,14 @@ units = [
|
||||
},
|
||||
{ 'file' : 'systemd-report-cgroup.socket' },
|
||||
{ 'file' : 'systemd-report-cgroup@.service.in' },
|
||||
{
|
||||
'file' : 'systemd-report-diskspace.socket',
|
||||
'conditions' : ['HAVE_LIBMOUNT'],
|
||||
},
|
||||
{
|
||||
'file' : 'systemd-report-diskspace@.service.in',
|
||||
'conditions' : ['HAVE_LIBMOUNT'],
|
||||
},
|
||||
{ 'file' : 'systemd-report-files.socket' },
|
||||
{ 'file' : 'systemd-report-files@.service.in' },
|
||||
{ 'file' : 'systemd-report-hwmon.socket' },
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# This file is part of systemd.
|
||||
#
|
||||
# systemd is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation; either version 2.1 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
[Unit]
|
||||
Description=Disk Space Report Varlink Socket
|
||||
Documentation=man:systemd-report-diskspace@.service(8)
|
||||
DefaultDependencies=no
|
||||
Before=sockets.target shutdown.target
|
||||
Conflicts=shutdown.target
|
||||
|
||||
[Socket]
|
||||
ListenStream=/run/systemd/report/io.systemd.DiskSpace
|
||||
FileDescriptorName=varlink
|
||||
SocketMode=0666
|
||||
Accept=yes
|
||||
MaxConnectionsPerSource=16
|
||||
RemoveOnStop=yes
|
||||
XAttrEntryPoint=user.varlink=entrypoint
|
||||
XAttrListen=user.varlink=listen
|
||||
XAttrAccept=user.varlink=server
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
@@ -0,0 +1,43 @@
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# This file is part of systemd.
|
||||
#
|
||||
# systemd is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation; either version 2.1 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
[Unit]
|
||||
Description=Disk Space Report Service
|
||||
Documentation=man:systemd-report-diskspace@.service(8)
|
||||
DefaultDependencies=no
|
||||
Conflicts=shutdown.target
|
||||
Before=shutdown.target
|
||||
|
||||
[Service]
|
||||
# Note that DynamicUser=, ProtectSystem=, ProtectHome= and PrivateTmp= are deliberately not used here: they
|
||||
# make the host's file systems appear read-only, or shadow them with private mounts, in the service's mount
|
||||
# namespace, and hence would hide from this service exactly what it is supposed to report on.
|
||||
CapabilityBoundingSet=
|
||||
DeviceAllow=
|
||||
IPAddressDeny=any
|
||||
LockPersonality=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
NoNewPrivileges=yes
|
||||
PrivateDevices=yes
|
||||
PrivateIPC=yes
|
||||
PrivateNetwork=yes
|
||||
ProtectControlGroups=yes
|
||||
ProtectHostname=yes
|
||||
ProtectKernelLogs=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
RestrictAddressFamilies=AF_UNIX
|
||||
RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
RestrictSUIDSGID=yes
|
||||
RuntimeMaxSec=1min
|
||||
SystemCallArchitectures=native
|
||||
SystemCallErrorNumber=EPERM
|
||||
SystemCallFilter=@system-service
|
||||
ExecStart={{LIBEXECDIR}}/systemd-report-diskspace
|
||||
Reference in New Issue
Block a user