From 652ba6e0dc6a2dd227f983d0ca93c3a39e9c2c93 Mon Sep 17 00:00:00 2001 From: Andres Beltran Date: Mon, 16 Jun 2025 23:38:25 +0000 Subject: [PATCH 1/7] chattr-util: add helpers to read and set project IDs --- src/basic/chattr-util.c | 79 +++++++++++++++++++++++++++++++++++++++++ src/basic/chattr-util.h | 4 +++ 2 files changed, 83 insertions(+) diff --git a/src/basic/chattr-util.c b/src/basic/chattr-util.c index 70a0210f72..8e15b56ea3 100644 --- a/src/basic/chattr-util.c +++ b/src/basic/chattr-util.c @@ -10,6 +10,7 @@ #include "fd-util.h" #include "fs-util.h" #include "log.h" +#include "recurse-dir.h" #include "string-util.h" int chattr_full( @@ -169,3 +170,81 @@ int read_attr_at(int dir_fd, const char *path, unsigned *ret) { return read_attr_fd(fd, ret); } + +int read_fs_xattr_fd(int fd, uint32_t *ret_xflags, uint32_t *ret_projid) { + struct fsxattr attrs; + _cleanup_close_ int fd_reopened = -EBADF; + + assert(fd >= 0); + + fd = fd_reopen_condition(fd, O_RDONLY|O_CLOEXEC|O_NOCTTY, O_PATH, &fd_reopened); + if (fd < 0) + return fd; + + if (ioctl(fd, FS_IOC_FSGETXATTR, &attrs) < 0) + return -errno; + + if (ret_xflags) + *ret_xflags = attrs.fsx_xflags; + + if (ret_projid) + *ret_projid = attrs.fsx_projid; + + return 0; +} + +int set_proj_id(int fd, uint32_t proj_id) { + struct fsxattr attrs; + _cleanup_close_ int fd_reopened = -EBADF; + + assert(fd >= 0); + + fd = fd_reopen_condition(fd, O_RDONLY|O_CLOEXEC|O_NOCTTY, O_PATH, &fd_reopened); + if (fd < 0) + return fd; + + if (ioctl(fd, FS_IOC_FSGETXATTR, &attrs) < 0) + return -errno; + + struct stat statbuf; + if (fstat(fd, &statbuf) < 0) + return -errno; + + if (attrs.fsx_projid == proj_id && (!S_ISDIR(statbuf.st_mode) || FLAGS_SET(attrs.fsx_xflags, FS_XFLAG_PROJINHERIT))) + return 0; + + attrs.fsx_projid = proj_id; + if (S_ISDIR(statbuf.st_mode)) + attrs.fsx_xflags |= FS_XFLAG_PROJINHERIT; + + return RET_NERRNO(ioctl(fd, FS_IOC_FSSETXATTR, &attrs)); +} + +static int set_proj_id_cb( + RecurseDirEvent event, + const char *path, + int dir_fd, + int inode_fd, + const struct dirent *de, + const struct statx *sx, + void *userdata) { + + if (!IN_SET(event, RECURSE_DIR_ENTER, RECURSE_DIR_ENTRY)) + return RECURSE_DIR_CONTINUE; + + if (de && !IN_SET(de->d_type, DT_DIR, DT_REG)) + return RECURSE_DIR_CONTINUE; + + return set_proj_id(inode_fd, PTR_TO_UINT32(userdata)); +} + +int set_proj_id_recursive(int fd, uint32_t proj_id) { + return recurse_dir_at( + fd, + /* path = */ NULL, + /* statx_mask = */ 0, + /* n_depth_max = */ UINT_MAX, + RECURSE_DIR_ENSURE_TYPE|RECURSE_DIR_TOPLEVEL|RECURSE_DIR_INODE_FD, + set_proj_id_cb, + UINT32_TO_PTR(proj_id)); +} diff --git a/src/basic/chattr-util.h b/src/basic/chattr-util.h index ab57239b08..50f97971f9 100644 --- a/src/basic/chattr-util.h +++ b/src/basic/chattr-util.h @@ -50,6 +50,10 @@ static inline int chattr_path(const char *path, unsigned value, unsigned mask) { int read_attr_fd(int fd, unsigned *ret); int read_attr_at(int dir_fd, const char *path, unsigned *ret); +int read_fs_xattr_fd(int fd, uint32_t *ret_xflags, uint32_t *ret_projid); + +int set_proj_id(int fd, uint32_t proj_id); +int set_proj_id_recursive(int fd, uint32_t proj_id); /* Combination of chattr flags, that should be appropriate for secrets stored on disk: Secure Remove + * Exclusion from Dumping + Synchronous Writing (i.e. not caching in memory) + In-Place Updating (i.e. not From 81e6b3685a1440a7662137f3abb8c86a42391189 Mon Sep 17 00:00:00 2001 From: Andres Beltran Date: Mon, 16 Jun 2025 23:39:25 +0000 Subject: [PATCH 2/7] quota-util: add methods to read and set project IDs --- src/shared/quota-util.c | 64 +++++++++++++++++++++++++++++++++++++++++ src/shared/quota-util.h | 3 ++ 2 files changed, 67 insertions(+) diff --git a/src/shared/quota-util.c b/src/shared/quota-util.c index a592b2fb00..06f27e85f2 100644 --- a/src/shared/quota-util.c +++ b/src/shared/quota-util.c @@ -5,6 +5,7 @@ #include "alloc-util.h" #include "blockdev-util.h" +#include "chattr-util.h" #include "device-util.h" #include "errno-util.h" #include "missing_syscall.h" @@ -33,3 +34,66 @@ int quotactl_fd_with_fallback(int fd, int cmd, int id, void *addr) { return RET_NERRNO(quotactl(cmd, devnode, id, addr)); } + +int quota_query_proj_id(int fd, uint32_t proj_id, struct dqblk *ret_req) { + int r; + + assert(fd >= 0); + assert(ret_req); + + r = quotactl_fd_with_fallback(fd, QCMD_FIXED(Q_GETQUOTA, PRJQUOTA), proj_id, ret_req); + if (r == -ESRCH || ERRNO_IS_NEG_NOT_SUPPORTED(r) || ERRNO_IS_NEG_PRIVILEGE(r)) { + zero(ret_req); + return false; + } + if (r < 0) + return r; + + return true; +} + +int quota_proj_id_set_recursive(int fd, uint32_t proj_id, bool verify_exclusive) { + int r; + + assert(fd >= 0); + + /* Confirm only the current inode has the project id (in case of race conditions) */ + if (verify_exclusive) { + /* Set to top level first because of the case where directories already exist with multiple subdirectories, + * in which case, number of inodes will be > 1 if applied recursively only */ + r = set_proj_id(fd, proj_id); + if (r < 0) + return r; + + struct dqblk req; + r = quotactl_fd_with_fallback(fd, QCMD_FIXED(Q_GETQUOTA, PRJQUOTA), proj_id, &req); + if (r < 0) + return r; + + if (req.dqb_curinodes == 0) + return -ENOTRECOVERABLE; + + if (req.dqb_curinodes != 1) + return false; + } + + r = set_proj_id_recursive(fd, proj_id); + if (r < 0) + return r; + + return true; +} + +bool quota_dqblk_is_populated(const struct dqblk *req) { + assert(req); + + return FLAGS_SET(req->dqb_valid, QIF_BLIMITS|QIF_SPACE|QIF_ILIMITS|QIF_INODES|QIF_BTIME|QIF_ITIME) && + (req->dqb_bhardlimit > 0 || + req->dqb_bsoftlimit > 0 || + req->dqb_ihardlimit > 0 || + req->dqb_isoftlimit > 0 || + req->dqb_curspace > 0 || + req->dqb_curinodes > 0 || + req->dqb_btime > 0 || + req->dqb_itime > 0); +} diff --git a/src/shared/quota-util.h b/src/shared/quota-util.h index 5e878a5d39..ec4da0cbb8 100644 --- a/src/shared/quota-util.h +++ b/src/shared/quota-util.h @@ -16,3 +16,6 @@ static inline int QCMD_FIXED(uint32_t cmd, uint32_t type) { } int quotactl_fd_with_fallback(int fd, int cmd, int id, void *addr); +int quota_query_proj_id(int fd, uint32_t proj_id, struct dqblk *ret_req); +int quota_proj_id_set_recursive(int fd, uint32_t proj_id, bool verify_exclusive); +bool quota_dqblk_is_populated(const struct dqblk *dqblk); From 744086b58dfc2e0b470c7eb71e305cfa6d0273af Mon Sep 17 00:00:00 2001 From: Andres Beltran Date: Tue, 1 Jul 2025 17:24:56 +0000 Subject: [PATCH 3/7] shared: add exec-directory-util.ch --- src/core/execute.c | 11 ----------- src/core/execute.h | 22 +++++++++------------- src/shared/exec-directory-util.c | 15 +++++++++++++++ src/shared/exec-directory-util.h | 19 +++++++++++++++++++ src/shared/meson.build | 1 + 5 files changed, 44 insertions(+), 24 deletions(-) create mode 100644 src/shared/exec-directory-util.c create mode 100644 src/shared/exec-directory-util.h diff --git a/src/core/execute.c b/src/core/execute.c index 28075441a4..5d5cc41207 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -3000,17 +3000,6 @@ static const char* const exec_preserve_mode_table[_EXEC_PRESERVE_MODE_MAX] = { DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(exec_preserve_mode, ExecPreserveMode, EXEC_PRESERVE_YES); -/* This table maps ExecDirectoryType to the setting it is configured with in the unit */ -static const char* const exec_directory_type_table[_EXEC_DIRECTORY_TYPE_MAX] = { - [EXEC_DIRECTORY_RUNTIME] = "RuntimeDirectory", - [EXEC_DIRECTORY_STATE] = "StateDirectory", - [EXEC_DIRECTORY_CACHE] = "CacheDirectory", - [EXEC_DIRECTORY_LOGS] = "LogsDirectory", - [EXEC_DIRECTORY_CONFIGURATION] = "ConfigurationDirectory", -}; - -DEFINE_STRING_TABLE_LOOKUP(exec_directory_type, ExecDirectoryType); - /* This table maps ExecDirectoryType to the symlink setting it is configured with in the unit */ static const char* const exec_directory_type_symlink_table[_EXEC_DIRECTORY_TYPE_MAX] = { [EXEC_DIRECTORY_RUNTIME] = "RuntimeDirectorySymlink", diff --git a/src/core/execute.h b/src/core/execute.h index a06f2f2eb3..e0f3ba51e0 100644 --- a/src/core/execute.h +++ b/src/core/execute.h @@ -7,6 +7,7 @@ #include "cgroup-util.h" #include "core-forward.h" #include "cpu-set-util.h" +#include "exec-directory-util.h" #include "exec-util.h" #include "list.h" #include "log-context.h" @@ -126,22 +127,19 @@ typedef struct ExecRuntime { int ephemeral_storage_socket[2]; } ExecRuntime; -typedef enum ExecDirectoryType { - EXEC_DIRECTORY_RUNTIME, - EXEC_DIRECTORY_STATE, - EXEC_DIRECTORY_CACHE, - EXEC_DIRECTORY_LOGS, - EXEC_DIRECTORY_CONFIGURATION, - _EXEC_DIRECTORY_TYPE_MAX, - _EXEC_DIRECTORY_TYPE_INVALID = -EINVAL, -} ExecDirectoryType; - static inline bool EXEC_DIRECTORY_TYPE_SHALL_CHOWN(ExecDirectoryType t) { /* Returns true for the ExecDirectoryTypes that we shall chown()ing for the user to. We do this for * all of them, except for configuration */ return t >= 0 && t < _EXEC_DIRECTORY_TYPE_MAX && t != EXEC_DIRECTORY_CONFIGURATION; } +typedef struct QuotaLimit { + uint64_t quota_absolute; /* absolute quota in bytes; if UINT64_MAX relative quota configured, see below */ + uint32_t quota_scale; /* relative quota to backend size, scaled to 0…UINT32_MAX */ + bool quota_enforce; + bool quota_accounting; +} QuotaLimit; + typedef struct ExecDirectoryItem { char *path; char **symlinks; @@ -153,6 +151,7 @@ typedef struct ExecDirectory { mode_t mode; size_t n_items; ExecDirectoryItem *items; + QuotaLimit exec_quota; } ExecDirectory; typedef enum ExecCleanMask { @@ -581,9 +580,6 @@ ExecPreserveMode exec_preserve_mode_from_string(const char *s) _pure_; const char* exec_keyring_mode_to_string(ExecKeyringMode i) _const_; ExecKeyringMode exec_keyring_mode_from_string(const char *s) _pure_; -const char* exec_directory_type_to_string(ExecDirectoryType i) _const_; -ExecDirectoryType exec_directory_type_from_string(const char *s) _pure_; - const char* exec_directory_type_symlink_to_string(ExecDirectoryType i) _const_; ExecDirectoryType exec_directory_type_symlink_from_string(const char *s) _pure_; diff --git a/src/shared/exec-directory-util.c b/src/shared/exec-directory-util.c new file mode 100644 index 0000000000..d9effa2502 --- /dev/null +++ b/src/shared/exec-directory-util.c @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "exec-directory-util.h" +#include "string-table.h" + +/* This table maps ExecDirectoryType to the setting it is configured with in the unit */ +static const char* const exec_directory_type_table[_EXEC_DIRECTORY_TYPE_MAX] = { + [EXEC_DIRECTORY_RUNTIME] = "RuntimeDirectory", + [EXEC_DIRECTORY_STATE] = "StateDirectory", + [EXEC_DIRECTORY_CACHE] = "CacheDirectory", + [EXEC_DIRECTORY_LOGS] = "LogsDirectory", + [EXEC_DIRECTORY_CONFIGURATION] = "ConfigurationDirectory", +}; + +DEFINE_STRING_TABLE_LOOKUP(exec_directory_type, ExecDirectoryType); diff --git a/src/shared/exec-directory-util.h b/src/shared/exec-directory-util.h new file mode 100644 index 0000000000..0006c73abf --- /dev/null +++ b/src/shared/exec-directory-util.h @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#include "macro-fundamental.h" + +typedef enum ExecDirectoryType { + EXEC_DIRECTORY_RUNTIME, + EXEC_DIRECTORY_STATE, + EXEC_DIRECTORY_CACHE, + EXEC_DIRECTORY_LOGS, + EXEC_DIRECTORY_CONFIGURATION, + _EXEC_DIRECTORY_TYPE_MAX, + _EXEC_DIRECTORY_TYPE_INVALID = -EINVAL, +} ExecDirectoryType; + +const char* exec_directory_type_to_string(ExecDirectoryType i) _const_; +ExecDirectoryType exec_directory_type_from_string(const char *s) _pure_; diff --git a/src/shared/meson.build b/src/shared/meson.build index 78e435de35..afbf7f8033 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -69,6 +69,7 @@ shared_sources = files( 'elf-util.c', 'enable-mempool.c', 'ethtool-util.c', + 'exec-directory-util.c', 'exec-util.c', 'exit-status.c', 'extension-util.c', From 26c6f3271a6c8e904379e69926ee9e5285589429 Mon Sep 17 00:00:00 2001 From: Andres Beltran Date: Tue, 1 Jul 2025 17:37:48 +0000 Subject: [PATCH 4/7] core: add quota support for State, Cache, and Log exec directories --- docs/DISK-QUOTAS-PROJECTIDS.md | 14 ++ docs/TRANSIENT-SETTINGS.md | 6 + man/org.freedesktop.systemd1.xml | 268 +++++++++++++++++++++++++- man/systemd.exec.xml | 40 ++++ src/core/exec-invoke.c | 230 ++++++++++++++++++++++ src/core/execute-serialize.c | 66 ++++++- src/core/load-fragment-gperf.gperf.in | 6 + src/core/load-fragment.c | 42 ++++ src/core/load-fragment.h | 1 + 9 files changed, 664 insertions(+), 9 deletions(-) create mode 100644 docs/DISK-QUOTAS-PROJECTIDS.md diff --git a/docs/DISK-QUOTAS-PROJECTIDS.md b/docs/DISK-QUOTAS-PROJECTIDS.md new file mode 100644 index 0000000000..11dc42bda2 --- /dev/null +++ b/docs/DISK-QUOTAS-PROJECTIDS.md @@ -0,0 +1,14 @@ +--- +title: Project IDs for Disk Quotas on Exec Directories +category: Exec directories +layout: default +SPDX-License-Identifier: LGPL-2.1-or-later +--- + +# Project IDs on systemd Systems + +Project IDs are needed to enforce disk quotas for Exec Directories. +Project IDs are unsigned, 32-bit integers. For disk quota enforcement, +the range used is 2147483648 - 4294967294, which is the highest range +inspired from `UIDS-GUID.md`. The range is defined through `PROJ_ID_MIN` +and `PROJ_ID_MAX` in `exec-invoke.c`. diff --git a/docs/TRANSIENT-SETTINGS.md b/docs/TRANSIENT-SETTINGS.md index d5c9a33df8..652ac3d95e 100644 --- a/docs/TRANSIENT-SETTINGS.md +++ b/docs/TRANSIENT-SETTINGS.md @@ -257,10 +257,16 @@ All execution-related settings are available for transient units. ✓ RuntimeDirectoryMode= ✓ RuntimeDirectory= ✓ StateDirectoryMode= +✓ StateDirectoryAccounting= +✓ StateDirectoryQuota= ✓ StateDirectory= ✓ CacheDirectoryMode= +✓ CacheDirectoryAccounting= +✓ CacheDirectoryQuota= ✓ CacheDirectory= ✓ LogsDirectoryMode= +✓ LogsDirectoryAccounting= +✓ LogsDirectoryQuota= ✓ LogsDirectory= ✓ ConfigurationDirectoryMode= ✓ ConfigurationDirectory= diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml index f5066e4834..27c3929a9c 100644 --- a/man/org.freedesktop.systemd1.xml +++ b/man/org.freedesktop.systemd1.xml @@ -3288,18 +3288,30 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly u StateDirectoryMode = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly b StateDirectoryAccounting = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly (tus) StateDirectoryQuota = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as StateDirectory = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(sst) CacheDirectorySymlink = [...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly u CacheDirectoryMode = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly b CacheDirectoryAccounting = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly (tus) CacheDirectoryQuota = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as CacheDirectory = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(sst) LogsDirectorySymlink = [...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly u LogsDirectoryMode = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly b LogsDirectoryAccounting = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly (tus) LogsDirectoryQuota = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as LogsDirectory = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly u ConfigurationDirectoryMode = ...; @@ -3351,6 +3363,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { readonly s MountImagePolicy = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly s ExtensionImagePolicy = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly (tt) StateDirectoryQuotaUsage = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly (tt) CacheDirectoryQuotaUsage = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly (tt) LogsDirectoryQuotaUsage = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly s KillMode = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") @@ -3885,10 +3903,22 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { + + + + + + + + + + + + @@ -3935,6 +3965,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { + + + + + + @@ -4579,18 +4615,30 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { + + + + + + + + + + + + @@ -4643,6 +4691,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { + + + + + + @@ -5443,18 +5497,30 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly u StateDirectoryMode = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly b StateDirectoryAccounting = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly (tus) StateDirectoryQuota = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as StateDirectory = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(sst) CacheDirectorySymlink = [...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly u CacheDirectoryMode = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly b CacheDirectoryAccounting = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly (tus) CacheDirectoryQuota = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as CacheDirectory = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(sst) LogsDirectorySymlink = [...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly u LogsDirectoryMode = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly b LogsDirectoryAccounting = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly (tus) LogsDirectoryQuota = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as LogsDirectory = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly u ConfigurationDirectoryMode = ...; @@ -5506,6 +5572,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { readonly s MountImagePolicy = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly s ExtensionImagePolicy = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly (tt) StateDirectoryQuotaUsage = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly (tt) CacheDirectoryQuotaUsage = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly (tt) LogsDirectoryQuotaUsage = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly s KillMode = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") @@ -6060,10 +6132,22 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { + + + + + + + + + + + + @@ -6110,6 +6194,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { + + + + + + @@ -6734,18 +6824,30 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { + + + + + + + + + + + + @@ -6798,6 +6900,12 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { + + + + + + @@ -7422,18 +7530,30 @@ node /org/freedesktop/systemd1/unit/home_2emount { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly u StateDirectoryMode = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly b StateDirectoryAccounting = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly (tus) StateDirectoryQuota = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as StateDirectory = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(sst) CacheDirectorySymlink = [...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly u CacheDirectoryMode = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly b CacheDirectoryAccounting = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly (tus) CacheDirectoryQuota = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as CacheDirectory = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(sst) LogsDirectorySymlink = [...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly u LogsDirectoryMode = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly b LogsDirectoryAccounting = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly (tus) LogsDirectoryQuota = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as LogsDirectory = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly u ConfigurationDirectoryMode = ...; @@ -7485,6 +7605,12 @@ node /org/freedesktop/systemd1/unit/home_2emount { readonly s MountImagePolicy = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly s ExtensionImagePolicy = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly (tt) StateDirectoryQuotaUsage = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly (tt) CacheDirectoryQuotaUsage = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly (tt) LogsDirectoryQuotaUsage = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly s KillMode = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") @@ -7961,10 +8087,22 @@ node /org/freedesktop/systemd1/unit/home_2emount { + + + + + + + + + + + + @@ -8011,6 +8149,12 @@ node /org/freedesktop/systemd1/unit/home_2emount { + + + + + + @@ -8543,18 +8687,30 @@ node /org/freedesktop/systemd1/unit/home_2emount { + + + + + + + + + + + + @@ -8607,6 +8763,12 @@ node /org/freedesktop/systemd1/unit/home_2emount { + + + + + + @@ -9364,18 +9526,30 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly u StateDirectoryMode = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly b StateDirectoryAccounting = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly (tus) StateDirectoryQuota = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as StateDirectory = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(sst) CacheDirectorySymlink = [...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly u CacheDirectoryMode = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly b CacheDirectoryAccounting = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly (tus) CacheDirectoryQuota = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as CacheDirectory = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(sst) LogsDirectorySymlink = [...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly u LogsDirectoryMode = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly b LogsDirectoryAccounting = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly (tus) LogsDirectoryQuota = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as LogsDirectory = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly u ConfigurationDirectoryMode = ...; @@ -9427,6 +9601,12 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap { readonly s MountImagePolicy = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly s ExtensionImagePolicy = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly (tt) StateDirectoryQuotaUsage = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly (tt) CacheDirectoryQuotaUsage = ...; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly (tt) LogsDirectoryQuotaUsage = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly s KillMode = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") @@ -9885,10 +10065,22 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap { + + + + + + + + + + + + @@ -9935,6 +10127,12 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap { + + + + + + @@ -10449,18 +10647,30 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap { + + + + + + + + + + + + @@ -10513,6 +10723,12 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap { + + + + + + @@ -12076,8 +12292,17 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \ PrivateUsersEx, and PrivatePIDs were added in version 257. ProtectHostnameEx, - DelegateNamespaces, and - RemoveSubGroup() were added in version 258. + DelegateNamespaces, + RemoveSubGroup(), + StateDirectoryQuota, + StateDirectoryQuotaUsage, + StateDirectoryAccounting, + CacheDirectoryQuota, + CacheDirectoryQuotaUsage, + CacheDirectoryAccounting, + LogsDirectoryQuota, + LogsDirectoryQuotaUsage, and + LogsDirectoryAccounting, were added in version 258. Socket Unit Objects @@ -12126,8 +12351,17 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \ AcceptFileDescriptors, DelegateNamespaces, RemoveSubgroup(), - DeferTrigger, and - DeferTriggerMaxUSec were added in version 258. + DeferTrigger, + DeferTriggerMaxUSec, + StateDirectoryQuota, + StateDirectoryQuotaUsage, + StateDirectoryAccounting, + CacheDirectoryQuota, + CacheDirectoryQuotaUsage, + CacheDirectoryAccounting, + LogsDirectoryQuota, + LogsDirectoryQuotaUsage, and + LogsDirectoryAccounting, were added in version 258. Mount Unit Objects @@ -12171,8 +12405,17 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \ ProtectHostnameEx, DelegateNamespaces, RemoveSubgroup(), - ReloadResult, and - CleanResult were added in version 258. + ReloadResult, + CleanResult, + StateDirectoryQuota, + StateDirectoryQuotaUsage, + StateDirectoryAccounting, + CacheDirectoryQuota, + CacheDirectoryQuotaUsage, + CacheDirectoryAccounting, + LogsDirectoryQuota, + LogsDirectoryQuotaUsage, and + LogsDirectoryAccounting, were added in version 258. Swap Unit Objects @@ -12214,8 +12457,17 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \ ProtectControlGroupsEx, and PrivatePIDs were added in version 257. ProtectHostnameEx, - DelegateNamespaces, and - RemoveSubgroup() were added in version 258. + DelegateNamespaces, + RemoveSubgroup(), + StateDirectoryQuota, + StateDirectoryQuotaUsage, + StateDirectoryAccounting, + CacheDirectoryQuota, + CacheDirectoryQuotaUsage, + CacheDirectoryAccounting, + LogsDirectoryQuota, + LogsDirectoryQuotaUsage, and + LogsDirectoryAccounting, were added in version 258. Slice Unit Objects diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml index f9e790e492..813ea02313 100644 --- a/man/systemd.exec.xml +++ b/man/systemd.exec.xml @@ -1680,6 +1680,46 @@ StateDirectory=aaa/bbb ccc + + StateDirectoryQuota= + CacheDirectoryQuota= + LogsDirectoryQuota= + + Specifies the storage limits for the directories specified in StateDirectory=, + CacheDirectory=, or LogsDirectory= respectively. + + The storage quota is defined in terms of disk blocks and inodes, as per + quotactl. Takes an absolute size limit + in bytes. If the value is suffixed with K, M, G or T, the specified size is parsed as Kilobytes, Megabytes, Gigabytes, + or Terabytes (with the base 1024), respectively. If an absolute size limit is specified, only the block quota is set + (rounded up to the nearest block). Alternatively, a percentage value may be specified, which applies the same percent + quota to both blocks and inodes. Defaults to off, in which case no storage limits will be set. + + Only hard limits are set, not soft limits. If the underlying filesystem for the specified directories does not + support project quotas, the specified storage limits will not be set. In addition to enabling per-unit quotas with + these settings, it is necessary to enable prjquota on the file system level as well + (i.e. tune2fs -Q prjquota). Quotas must also be turned on with + quotaon. + + + + + + StateDirectoryAccounting= + CacheDirectoryAccounting= + LogsDirectoryAccounting= + + Takes a boolean argument. If true, a project ID is assigned to the directories specified in + StateDirectory=, CacheDirectory=, or LogsDirectory= + respectively, which is used for tracking disk usage when disk quotas are turned on + (see repquota). Defaults to false. + + To set and enforce disk quotas, StateDirectoryQuota=, CacheDirectoryQuota=, + or LogsDirectoryQuota= must be specified. + + + + RuntimeDirectoryPreserve= diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index deb88e7f6b..0f42868546 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #if HAVE_PAM @@ -30,6 +31,7 @@ #include "cgroup-setup.h" #include "cgroup.h" #include "chase.h" +#include "chattr-util.h" #include "chown-recursive.h" #include "constants.h" #include "copy.h" @@ -60,15 +62,19 @@ #include "open-file.h" #include "osc-context.h" #include "path-util.h" +#include "percent-util.h" #include "pidref.h" #include "proc-cmdline.h" #include "process-util.h" #include "psi-util.h" +#include "quota-util.h" +#include "random-util.h" #include "rlimit-util.h" #include "seccomp-util.h" #include "selinux-util.h" #include "set.h" #include "signal-util.h" +#include "siphash24.h" #include "smack-util.h" #include "socket-util.h" #include "stat-util.h" @@ -84,6 +90,11 @@ #define SNDBUF_SIZE (8*1024*1024) +/* Project id range for disk quotas */ +#define PROJ_ID_MIN UINT32_C(2147483648) +#define PROJ_ID_MAX UINT32_C(4294967294) +#define PROJ_ID_CLAMP_INTO_QUOTA_RANGE(id) ((uint32_t) ((id) % (PROJ_ID_MAX - PROJ_ID_MIN + 1)) + PROJ_ID_MIN) + static int flag_fds( const int fds[], size_t n_socket_fds, @@ -2575,6 +2586,217 @@ static int create_many_symlinks(const char *root, const char *source, char **sym return 0; } +static int set_exec_storage_quota(int fd, uint32_t proj_id, const QuotaLimit *ql) { + int r; + uint64_t block_limit = 0, inode_limit = 0; + + assert(fd >= 0); + assert(ql); + + if (ql->quota_absolute == 0 || ql->quota_scale == 0) + /* Limit of 0 means no usage is allowed. For quotactl, use 1 as the limit, since 0 means that + * hard limits are disabled */ + block_limit = inode_limit = 1; + else if (ql->quota_absolute == UINT64_MAX) { + _cleanup_close_ int fd_parent = -EBADF; + + /* Use target_dir's parent when setting quotas. If a FD for target_dir has been previously + * used for quotactl_fd(SET) and is passed again for fstatvfs(), the total number of blocks is not + * reported accurately (instead, the block limit is reported as total blocks). Thus, use the FD + * associated with the parent, so that total blocks is accurate */ + fd_parent = openat(fd, "..", O_PATH|O_CLOEXEC|O_DIRECTORY); + if (fd_parent < 0) + return -errno; + + uint32_t xattr_flags = 0; + r = read_fs_xattr_fd(fd_parent, &xattr_flags, /* ret_projid = */ NULL); + if (r < 0) + return r; + /* Refuse if parent has FS_XFLAG_PROJINHERIT since this will mean the total number of blocks will not + * be reported accurately */ + if (FLAGS_SET(xattr_flags, FS_XFLAG_PROJINHERIT)) + return -ENOMEDIUM; + + struct statvfs disk_st; + if (fstatvfs(fd_parent, &disk_st) < 0) + return -errno; + + block_limit = (uint64_t) DIV_ROUND_UP((uint64_t)((double) (disk_st.f_frsize * disk_st.f_blocks) / UINT32_MAX * ql->quota_scale), QIF_DQBLKSIZE); + inode_limit = (uint64_t) ((double) disk_st.f_files / UINT32_MAX * ql->quota_scale); + } else + block_limit = (uint64_t) DIV_ROUND_UP(ql->quota_absolute, QIF_DQBLKSIZE); + + struct dqblk req = { + .dqb_bhardlimit = block_limit, + .dqb_ihardlimit = inode_limit, + .dqb_valid = QIF_LIMITS, + }; + + r = quotactl_fd_with_fallback(fd, QCMD_FIXED(Q_SETQUOTA, PRJQUOTA), proj_id, &req); + if (r < 0) + return r; + + log_debug("Storage quotas set for project id %" PRIu32 ". Block limit = %" PRIu64 ", inode limit = %" PRIu64, proj_id, block_limit, inode_limit); + + return 0; +} + +static int unset_exec_storage_quota(int fd, uint32_t proj_id, bool quota_accounting) { + int r, quota_supported; + struct dqblk req; + + assert(fd >= 0); + + quota_supported = quota_query_proj_id(fd, proj_id, &req); + if (quota_supported < 0) + return log_debug_errno(quota_supported, "Failed to query disk quota for project ID %" PRIu32 ": %m", proj_id); + + /* Do not enforce quotas anymore */ + if (quota_supported && FLAGS_SET(req.dqb_valid, QIF_BLIMITS) && (req.dqb_bhardlimit > 0 || req.dqb_ihardlimit > 0)) { + req.dqb_bhardlimit = 0, req.dqb_ihardlimit = 0; + + r = quotactl_fd_with_fallback(fd, QCMD_FIXED(Q_SETQUOTA, PRJQUOTA), proj_id, &req); + if (r < 0) + return log_debug_errno(r, "Failed to disable project quotas for project ID %" PRIu32 ": %m", proj_id); + + log_debug("Storage quotas for project ID %" PRIu32 " were disabled", proj_id); + } + + /* Release project ID if no accounting needed */ + if (!quota_accounting) { + r = set_proj_id_recursive(fd, 0); + if (r < 0) + log_warning_errno(r, "Failed to release project ID %" PRIu32 ", ignoring: %m", proj_id); + } + + return 0; +} + +static int apply_exec_quotas( + const char *target_dir, + const char *cgroup_path, + ExecDirectoryType type, + const QuotaLimit *ql, + uint32_t *exec_dt_proj_id, /* in/out */ + bool *already_enforced) { /* in/out */ + + _cleanup_close_ int fd = -EBADF; + int r, quota_supported = 0; + + assert(target_dir); + assert(cgroup_path); + assert(ql); + assert(exec_dt_proj_id); + assert(already_enforced); + + /* Do not apply to the Runtime directory since tmpfs does not support project IDs yet */ + if (!IN_SET(type, EXEC_DIRECTORY_STATE, EXEC_DIRECTORY_CACHE, EXEC_DIRECTORY_LOGS)) + return 0; + + fd = open(target_dir, O_PATH|O_CLOEXEC|O_DIRECTORY); + if (fd < 0) + return log_debug_errno(errno, "Failed to open %s: %m", target_dir); + + /* Get the project ID of the current directory */ + uint32_t proj_id; + r = read_fs_xattr_fd(fd, /* ret_xflags = */ NULL, &proj_id); + if (ERRNO_IS_NEG_IOCTL_NOT_SUPPORTED(r)) { + log_debug_errno(r, "Not applying storage quotas. FS_IOC_FSGETXATTR not supported for %s: %m", target_dir); + return 0; + } + if (r < 0) + return log_debug_errno(r, "Failed to retrieve project ID for %s: %m", target_dir); + + /* If the first directory of this ExecType already has a project ID, adopt it as the project ID for all dirs of this ExecType */ + bool proj_id_exists = PROJ_ID_MIN <= proj_id && proj_id <= PROJ_ID_MAX; + if (proj_id_exists && *exec_dt_proj_id == 0) + *exec_dt_proj_id = proj_id; + + /* Check if enforcement should be disabled. Do not release project ID if accounting is enabled */ + if (!ql->quota_enforce) { + if (proj_id_exists) { + r = unset_exec_storage_quota(fd, proj_id, ql->quota_accounting); + if (r < 0) + return log_debug_errno(r, "Failed to unset project quotas for %s: %m", target_dir); + } + + if (!ql->quota_accounting) + return 0; + } + + if (*exec_dt_proj_id > 0 && *exec_dt_proj_id != proj_id) { + /* Set the existing project ID only if the current directory's ID does not exist or does not match */ + proj_id = *exec_dt_proj_id; + r = quota_proj_id_set_recursive(fd, proj_id, false); + if (r < 0) + return log_debug_errno(r, "Failed to set project ID for %s: %m", target_dir); + } else if (*exec_dt_proj_id == 0) { + /* Only generate a new project ID if it's the first directory of this ExecType to be processed and does not have an existing ID */ + static const sd_id128_t k = SD_ID128_ARRAY(e1,4a,79,9b,64,40,41,4a,a8,46,c2,f3,f9,19,4f,01); + _cleanup_free_ char *proj_id_plain = NULL; + + /* Generate candidate project id */ + proj_id_plain = strjoin(cgroup_path, "|", exec_directory_type_to_string(type)); + if (!proj_id_plain) + return log_oom_debug(); + + struct siphash state; + siphash24_init(&state, k.bytes); + siphash24_compress_string(proj_id_plain, &state); + proj_id = PROJ_ID_CLAMP_INTO_QUOTA_RANGE(siphash24_finalize(&state)); + +#define MAX_PROJ_ID_RETRIES 10 + for (unsigned attempt = 0;; attempt++) { + if (attempt >= MAX_PROJ_ID_RETRIES) + return log_debug_errno(SYNTHETIC_ERRNO(EBUSY), "Failed to generate unique project ID for %s: %m", target_dir); + + /* Check if project quotas are supported */ + struct dqblk req; + quota_supported = quota_query_proj_id(fd, proj_id, &req); + if (quota_supported < 0) + return log_debug_errno(quota_supported, "Failed to query disk quota for project ID %" PRIu32 ": %m", proj_id); + if (!quota_supported) { + log_debug("Not applying storage quotas. Project quotas are not supported for %s", target_dir); + return 0; + } + + if (!quota_dqblk_is_populated(&req)) { + int proj_id_was_set = quota_proj_id_set_recursive(fd, proj_id, true); + if (proj_id_was_set < 0) + return log_debug_errno(proj_id_was_set, "Failed to set project ID for %s: %m", target_dir); + if (proj_id_was_set) { + *exec_dt_proj_id = proj_id; + log_debug("Project ID %u generated for %s", proj_id, target_dir); + break; + } + } + + proj_id = (uint32_t) (random_u64_range(PROJ_ID_MAX - PROJ_ID_MIN + 1) + PROJ_ID_MIN); + } + } + + if (ql->quota_enforce && !*already_enforced) { + if (!quota_supported) { + struct dqblk req; + quota_supported = quota_query_proj_id(fd, proj_id, &req); + if (quota_supported < 0) + return log_debug_errno(quota_supported, "Failed to query disk quota for project ID %" PRIu32 ": %m", proj_id); + if (!quota_supported) { + log_debug("Not applying storage quotas. Project quotas are not supported for %s", target_dir); + return 0; + } + } + + r = set_exec_storage_quota(fd, proj_id, ql); + if (r < 0) + return log_debug_errno(r, "Failed to set storage quotas for %s: %m", target_dir); + + *already_enforced = true; + } + + return r; +} + static int setup_exec_directory( const ExecContext *context, const ExecParameters *params, @@ -2608,6 +2830,9 @@ static int setup_exec_directory( gid = 0; } + uint32_t exec_dt_proj_id = 0; + bool quota_already_enforced = false; + FOREACH_ARRAY(i, context->directories[type].items, context->directories[type].n_items) { _cleanup_free_ char *p = NULL, *pp = NULL; @@ -2898,6 +3123,11 @@ static int setup_exec_directory( if (r < 0) goto fail; } + + /* Apply storage quotas and accounting */ + r = apply_exec_quotas(target_dir, params->cgroup_path, type, &context->directories[type].exec_quota, &exec_dt_proj_id, "a_already_enforced); + if (r < 0) + goto fail; } /* If we are not going to run in a namespace, set up the symlinks - otherwise diff --git a/src/core/execute-serialize.c b/src/core/execute-serialize.c index 9110d3447f..0a1af05e51 100644 --- a/src/core/execute-serialize.c +++ b/src/core/execute-serialize.c @@ -1848,6 +1848,28 @@ static int exec_context_serialize(const ExecContext *c, FILE *f) { r = serialize_item(f, key, value); if (r < 0) return r; + + if (c->directories[dt].exec_quota.quota_enforce) { + _cleanup_free_ char *key_quota = NULL; + key_quota = strjoin("exec-context-quota-directories-", exec_directory_type_to_string(dt)); + if (!key_quota) + return log_oom_debug(); + + r = serialize_item_format(f, key_quota, "%" PRIu64 " %" PRIu32, c->directories[dt].exec_quota.quota_absolute, + c->directories[dt].exec_quota.quota_scale); + if (r < 0) + return r; + + } else if (c->directories[dt].exec_quota.quota_accounting) { + _cleanup_free_ char *key_quota = NULL; + key_quota = strjoin("exec-context-quota-accounting-directories-", exec_directory_type_to_string(dt)); + if (!key_quota) + return log_oom_debug(); + + r = serialize_bool(f, key_quota, c->directories[dt].exec_quota.quota_accounting); + if (r < 0) + return r; + } } r = serialize_usec(f, "exec-context-timeout-clean-usec", c->timeout_clean_usec); @@ -2735,7 +2757,7 @@ static int exec_context_deserialize(ExecContext *c, FILE *f) { dt = exec_directory_type_from_string(type); if (dt < 0) - return -EINVAL; + return dt; r = parse_mode(mode, &c->directories[dt].mode); if (r < 0) @@ -2793,6 +2815,48 @@ static int exec_context_deserialize(ExecContext *c, FILE *f) { return r; } } + } else if ((val = startswith(l, "exec-context-quota-accounting-directories-"))) { + _cleanup_free_ char *type = NULL, *quota_accounting = NULL; + ExecDirectoryType dt; + + r = split_pair(val, "=", &type, "a_accounting); + if (r < 0) + return r; + + dt = exec_directory_type_from_string(type); + if (dt < 0) + return dt; + + r = parse_boolean(quota_accounting); + if (r < 0) + return r; + + c->directories[dt].exec_quota.quota_accounting = r; + } else if ((val = startswith(l, "exec-context-quota-directories-"))) { + _cleanup_free_ char *type = NULL, *quota_info = NULL, *quota_absolute = NULL, *quota_scale = NULL; + ExecDirectoryType dt; + + r = split_pair(val, "=", &type, "a_info); + if (r < 0) + return r; + + r = split_pair(quota_info, " ", "a_absolute, "a_scale); + if (r < 0) + return r; + + dt = exec_directory_type_from_string(type); + if (dt < 0) + return dt; + + r = safe_atou64(quota_absolute, &c->directories[dt].exec_quota.quota_absolute); + if (r < 0) + return r; + + r = safe_atou32(quota_scale, &c->directories[dt].exec_quota.quota_scale); + if (r < 0) + return r; + + c->directories[dt].exec_quota.quota_enforce = true; } else if ((val = startswith(l, "exec-context-timeout-clean-usec="))) { r = deserialize_usec(val, &c->timeout_clean_usec); if (r < 0) diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in index 96fe04aaf4..7d4d174d84 100644 --- a/src/core/load-fragment-gperf.gperf.in +++ b/src/core/load-fragment-gperf.gperf.in @@ -145,10 +145,16 @@ {{type}}.RuntimeDirectoryMode, config_parse_mode, 0, offsetof({{type}}, exec_context.directories[EXEC_DIRECTORY_RUNTIME].mode) {{type}}.RuntimeDirectory, config_parse_exec_directories, 0, offsetof({{type}}, exec_context.directories[EXEC_DIRECTORY_RUNTIME]) {{type}}.StateDirectoryMode, config_parse_mode, 0, offsetof({{type}}, exec_context.directories[EXEC_DIRECTORY_STATE].mode) +{{type}}.StateDirectoryAccounting, config_parse_bool, 0, offsetof({{type}}, exec_context.directories[EXEC_DIRECTORY_STATE].exec_quota.quota_accounting) +{{type}}.StateDirectoryQuota, config_parse_exec_quota, 0, offsetof({{type}}, exec_context.directories[EXEC_DIRECTORY_STATE].exec_quota) {{type}}.StateDirectory, config_parse_exec_directories, 0, offsetof({{type}}, exec_context.directories[EXEC_DIRECTORY_STATE]) {{type}}.CacheDirectoryMode, config_parse_mode, 0, offsetof({{type}}, exec_context.directories[EXEC_DIRECTORY_CACHE].mode) +{{type}}.CacheDirectoryAccounting, config_parse_bool, 0, offsetof({{type}}, exec_context.directories[EXEC_DIRECTORY_CACHE].exec_quota.quota_accounting) +{{type}}.CacheDirectoryQuota, config_parse_exec_quota, 0, offsetof({{type}}, exec_context.directories[EXEC_DIRECTORY_CACHE].exec_quota) {{type}}.CacheDirectory, config_parse_exec_directories, 0, offsetof({{type}}, exec_context.directories[EXEC_DIRECTORY_CACHE]) {{type}}.LogsDirectoryMode, config_parse_mode, 0, offsetof({{type}}, exec_context.directories[EXEC_DIRECTORY_LOGS].mode) +{{type}}.LogsDirectoryAccounting, config_parse_bool, 0, offsetof({{type}}, exec_context.directories[EXEC_DIRECTORY_LOGS].exec_quota.quota_accounting) +{{type}}.LogsDirectoryQuota, config_parse_exec_quota, 0, offsetof({{type}}, exec_context.directories[EXEC_DIRECTORY_LOGS].exec_quota) {{type}}.LogsDirectory, config_parse_exec_directories, 0, offsetof({{type}}, exec_context.directories[EXEC_DIRECTORY_LOGS]) {{type}}.ConfigurationDirectoryMode, config_parse_mode, 0, offsetof({{type}}, exec_context.directories[EXEC_DIRECTORY_CONFIGURATION].mode) {{type}}.ConfigurationDirectory, config_parse_exec_directories, 0, offsetof({{type}}, exec_context.directories[EXEC_DIRECTORY_CONFIGURATION]) diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c index 3705e95884..c1e704b1c6 100644 --- a/src/core/load-fragment.c +++ b/src/core/load-fragment.c @@ -4602,6 +4602,48 @@ int config_parse_exec_directories( } } +int config_parse_exec_quota( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + QuotaLimit *quota_limit = ASSERT_PTR(data); + uint64_t quota_absolute = UINT64_MAX; + uint32_t quota_scale = UINT32_MAX; + int r; + + if (isempty(rvalue) || streq(rvalue, "off")) { + quota_limit->quota_enforce = false; + quota_limit->quota_absolute = UINT64_MAX; + quota_limit->quota_scale = UINT32_MAX; + return 0; + } + + r = parse_permyriad(rvalue); + if (r < 0) { + r = parse_size(rvalue, 1024, "a_absolute); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse disk quota value, ignoring: %s", rvalue); + return 0; + } + } else + /* Normalize to 2^32-1 == 100% */ + quota_scale = UINT32_SCALE_FROM_PERMYRIAD(r); + + quota_limit->quota_absolute = quota_absolute; + quota_limit->quota_scale = quota_scale; + quota_limit->quota_enforce = true; + + return 0; +} + int config_parse_set_credential( const char *unit, const char *filename, diff --git a/src/core/load-fragment.h b/src/core/load-fragment.h index e2fe7dce1b..a31ad750d3 100644 --- a/src/core/load-fragment.h +++ b/src/core/load-fragment.h @@ -101,6 +101,7 @@ CONFIG_PARSER_PROTOTYPE(config_parse_exec_smack_process_label); CONFIG_PARSER_PROTOTYPE(config_parse_address_families); CONFIG_PARSER_PROTOTYPE(config_parse_exec_preserve_mode); CONFIG_PARSER_PROTOTYPE(config_parse_exec_directories); +CONFIG_PARSER_PROTOTYPE(config_parse_exec_quota); CONFIG_PARSER_PROTOTYPE(config_parse_set_credential); CONFIG_PARSER_PROTOTYPE(config_parse_load_credential); CONFIG_PARSER_PROTOTYPE(config_parse_import_credential); From a89afe194831322a9941dda5189a7c84e7b2cb0e Mon Sep 17 00:00:00 2001 From: Andres Beltran Date: Tue, 1 Jul 2025 17:40:47 +0000 Subject: [PATCH 5/7] Add quota support for DBus --- src/core/dbus-execute.c | 128 ++++++++++++++++++ src/core/dbus-execute.h | 1 + src/core/dbus.c | 29 +++- src/core/unit.c | 48 +++++++ src/core/unit.h | 2 + src/shared/bus-unit-util.c | 31 +++++ .../fuzz-unit-file/directives-all.service | 6 + 7 files changed, 243 insertions(+), 2 deletions(-) diff --git a/src/core/dbus-execute.c b/src/core/dbus-execute.c index 1a08c8867e..d1cb46a169 100644 --- a/src/core/dbus-execute.c +++ b/src/core/dbus-execute.c @@ -31,7 +31,9 @@ #include "namespace.h" #include "nsflags.h" #include "ordered-set.h" +#include "parse-util.h" #include "path-util.h" +#include "percent-util.h" #include "pcre2-util.h" #include "process-util.h" #include "rlimit-util.h" @@ -1000,6 +1002,22 @@ static int property_get_exec_dir_symlink( return sd_bus_message_close_container(reply); } +static int property_get_exec_quota(sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + QuotaLimit *q = ASSERT_PTR(userdata); + + assert(bus); + assert(reply); + + return sd_bus_message_append(reply, "(tus)", q->quota_absolute, q->quota_scale, yes_no(q->quota_enforce)); +} + static int property_get_image_policy( sd_bus *bus, const char *path, @@ -1266,12 +1284,18 @@ const sd_bus_vtable bus_exec_vtable[] = { SD_BUS_PROPERTY("RuntimeDirectory", "as", property_get_exec_dir, offsetof(ExecContext, directories[EXEC_DIRECTORY_RUNTIME]), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("StateDirectorySymlink", "a(sst)", property_get_exec_dir_symlink, offsetof(ExecContext, directories[EXEC_DIRECTORY_STATE]), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("StateDirectoryMode", "u", bus_property_get_mode, offsetof(ExecContext, directories[EXEC_DIRECTORY_STATE].mode), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("StateDirectoryAccounting", "b", bus_property_get_bool, offsetof(ExecContext, directories[EXEC_DIRECTORY_STATE].exec_quota.quota_accounting), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("StateDirectoryQuota", "(tus)", property_get_exec_quota, offsetof(ExecContext, directories[EXEC_DIRECTORY_STATE].exec_quota), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("StateDirectory", "as", property_get_exec_dir, offsetof(ExecContext, directories[EXEC_DIRECTORY_STATE]), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("CacheDirectorySymlink", "a(sst)", property_get_exec_dir_symlink, offsetof(ExecContext, directories[EXEC_DIRECTORY_CACHE]), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("CacheDirectoryMode", "u", bus_property_get_mode, offsetof(ExecContext, directories[EXEC_DIRECTORY_CACHE].mode), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("CacheDirectoryAccounting", "b", bus_property_get_bool, offsetof(ExecContext, directories[EXEC_DIRECTORY_CACHE].exec_quota.quota_accounting), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("CacheDirectoryQuota", "(tus)", property_get_exec_quota, offsetof(ExecContext, directories[EXEC_DIRECTORY_CACHE].exec_quota), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("CacheDirectory", "as", property_get_exec_dir, offsetof(ExecContext, directories[EXEC_DIRECTORY_CACHE]), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("LogsDirectorySymlink", "a(sst)", property_get_exec_dir_symlink, offsetof(ExecContext, directories[EXEC_DIRECTORY_LOGS]), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("LogsDirectoryMode", "u", bus_property_get_mode, offsetof(ExecContext, directories[EXEC_DIRECTORY_LOGS].mode), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("LogsDirectoryAccounting", "b", bus_property_get_bool, offsetof(ExecContext, directories[EXEC_DIRECTORY_LOGS].exec_quota.quota_accounting), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("LogsDirectoryQuota", "(tus)", property_get_exec_quota, offsetof(ExecContext, directories[EXEC_DIRECTORY_LOGS].exec_quota), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("LogsDirectory", "as", property_get_exec_dir, offsetof(ExecContext, directories[EXEC_DIRECTORY_LOGS]), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("ConfigurationDirectoryMode", "u", bus_property_get_mode, offsetof(ExecContext, directories[EXEC_DIRECTORY_CONFIGURATION].mode), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("ConfigurationDirectory", "as", property_get_exec_dir, offsetof(ExecContext, directories[EXEC_DIRECTORY_CONFIGURATION]), SD_BUS_VTABLE_PROPERTY_CONST), @@ -1309,6 +1333,60 @@ const sd_bus_vtable bus_exec_vtable[] = { SD_BUS_VTABLE_END }; +static int property_get_quota_usage( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Unit *u = ASSERT_PTR(userdata); + ExecContext *c = ASSERT_PTR(unit_get_exec_context(u)); + uint64_t current_usage_bytes = UINT64_MAX, limit_bytes = UINT64_MAX; + int r; + + assert(bus); + assert(reply); + + ExecDirectoryType dt; + if (streq(property, "StateDirectoryQuotaUsage")) + dt = EXEC_DIRECTORY_STATE; + else if (streq(property, "CacheDirectoryQuotaUsage")) + dt = EXEC_DIRECTORY_CACHE; + else if (streq(property, "LogsDirectoryQuotaUsage")) + dt = EXEC_DIRECTORY_LOGS; + else + assert_not_reached(); + + const QuotaLimit *q; + q = &c->directories[dt].exec_quota; + + if (q->quota_enforce || q->quota_accounting) { + r = unit_get_exec_quota_stats(u, c, dt, ¤t_usage_bytes, &limit_bytes); + if (r < 0) + return r; + } + + if (!q->quota_enforce) + limit_bytes = UINT64_MAX; + if (!q->quota_accounting) + current_usage_bytes = UINT64_MAX; + + return sd_bus_message_append(reply, "(tt)", current_usage_bytes, limit_bytes); +} + +const sd_bus_vtable bus_unit_exec_vtable[] = { + SD_BUS_VTABLE_START(0), + + SD_BUS_PROPERTY("StateDirectoryQuotaUsage", "(tt)", property_get_quota_usage, 0, 0), + SD_BUS_PROPERTY("CacheDirectoryQuotaUsage", "(tt)", property_get_quota_usage, 0, 0), + SD_BUS_PROPERTY("LogsDirectoryQuotaUsage", "(tt)", property_get_quota_usage, 0, 0), + + SD_BUS_VTABLE_END +}; + static int append_exec_command(sd_bus_message *reply, ExecCommand *c) { int r; @@ -2210,6 +2288,15 @@ int bus_exec_context_set_transient_property( if (streq(name, "RuntimeDirectoryMode")) return bus_set_transient_mode_t(u, name, &c->directories[EXEC_DIRECTORY_RUNTIME].mode, message, flags, error); + if (streq(name, "StateDirectoryAccounting")) + return bus_set_transient_bool(u, name, &c->directories[EXEC_DIRECTORY_STATE].exec_quota.quota_accounting, message, flags, error); + + if (streq(name, "CacheDirectoryAccounting")) + return bus_set_transient_bool(u, name, &c->directories[EXEC_DIRECTORY_CACHE].exec_quota.quota_accounting, message, flags, error); + + if (streq(name, "LogsDirectoryAccounting")) + return bus_set_transient_bool(u, name, &c->directories[EXEC_DIRECTORY_LOGS].exec_quota.quota_accounting, message, flags, error); + if (streq(name, "StateDirectoryMode")) return bus_set_transient_mode_t(u, name, &c->directories[EXEC_DIRECTORY_STATE].mode, message, flags, error); @@ -3603,6 +3690,47 @@ int bus_exec_context_set_transient_property( return 1; + } else if (STR_IN_SET(name, "StateDirectoryQuota", "CacheDirectoryQuota", "LogsDirectoryQuota")) { + uint64_t quota_absolute = UINT64_MAX; + uint32_t quota_scale = UINT32_MAX; + const char *enforce_flag; + int quota_enforce; + + r = sd_bus_message_read(message, "(tus)", "a_absolute, "a_scale, &enforce_flag); + if (r < 0) + return r; + + quota_enforce = parse_boolean(enforce_flag); + if (quota_enforce < 0) + return quota_enforce; + + if (!UNIT_WRITE_FLAGS_NOOP(flags)) { + ExecDirectoryType dt; + if (streq(name, "StateDirectoryQuota")) + dt = EXEC_DIRECTORY_STATE; + else if (streq(name, "CacheDirectoryQuota")) + dt = EXEC_DIRECTORY_CACHE; + else if (streq(name, "LogsDirectoryQuota")) + dt = EXEC_DIRECTORY_LOGS; + else + assert_not_reached(); + + if (quota_enforce) { + c->directories[dt].exec_quota.quota_absolute = quota_absolute; + c->directories[dt].exec_quota.quota_scale = quota_scale; + + if (quota_absolute != UINT64_MAX) + unit_write_settingf(u, flags, name, "%s=%" PRIu64, name, quota_absolute); + else + unit_write_settingf(u, flags, name, "%s=%d%%", name, UINT32_SCALE_TO_PERCENT(quota_scale)); + } else + unit_write_settingf(u, flags, name, "%s=", name); + + c->directories[dt].exec_quota.quota_enforce = quota_enforce; + } + + return 1; + } else if (STR_IN_SET(name, "AppArmorProfile", "SmackProcessLabel")) { int ignore; const char *s; diff --git a/src/core/dbus-execute.h b/src/core/dbus-execute.h index c3566bda56..38b9c2ab0f 100644 --- a/src/core/dbus-execute.h +++ b/src/core/dbus-execute.h @@ -24,6 +24,7 @@ SD_BUS_PROPERTY(name, "a(sasasttttuii)", bus_property_get_exec_ex_command_list, offset, flags) extern const sd_bus_vtable bus_exec_vtable[]; +extern const sd_bus_vtable bus_unit_exec_vtable[]; int bus_property_get_exec_output(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error); int bus_property_get_exec_command(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *userdata, sd_bus_error *ret_error); diff --git a/src/core/dbus.c b/src/core/dbus.c index 223c0b79d2..1659089dd9 100644 --- a/src/core/dbus.c +++ b/src/core/dbus.c @@ -338,9 +338,8 @@ static int bus_cgroup_context_find(sd_bus *bus, const char *path, const char *in return 1; } -static int bus_exec_context_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error) { +static int bus_unit_exec_context_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error) { Manager *m = ASSERT_PTR(userdata); - ExecContext *c; Unit *u; int r; @@ -356,6 +355,27 @@ static int bus_exec_context_find(sd_bus *bus, const char *path, const char *inte if (!streq_ptr(interface, unit_dbus_interface_from_type(u->type))) return 0; + if (!UNIT_HAS_EXEC_CONTEXT(u)) + return 0; + + *found = u; + return 1; +} + +static int bus_exec_context_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error) { + ExecContext *c; + int r; + + assert(bus); + assert(path); + assert(interface); + assert(found); + + Unit *u; + r = bus_unit_exec_context_find(bus, path, interface, userdata, (void**) &u, error); + if (r <= 0) + return r; + c = unit_get_exec_context(u); if (!c) return 0; @@ -443,6 +463,7 @@ static const BusObjectImplementation bus_mount_object = { { bus_unit_cgroup_vtable, bus_unit_cgroup_find }, { bus_cgroup_vtable, bus_cgroup_context_find }, { bus_exec_vtable, bus_exec_context_find }, + { bus_unit_exec_vtable, bus_unit_exec_context_find }, { bus_kill_vtable, bus_kill_context_find }), }; @@ -471,6 +492,7 @@ static const BusObjectImplementation bus_service_object = { { bus_unit_cgroup_vtable, bus_unit_cgroup_find }, { bus_cgroup_vtable, bus_cgroup_context_find }, { bus_exec_vtable, bus_exec_context_find }, + { bus_unit_exec_vtable, bus_unit_exec_context_find }, { bus_kill_vtable, bus_kill_context_find }), }; @@ -491,6 +513,7 @@ static const BusObjectImplementation bus_socket_object = { { bus_unit_cgroup_vtable, bus_unit_cgroup_find }, { bus_cgroup_vtable, bus_cgroup_context_find }, { bus_exec_vtable, bus_exec_context_find }, + { bus_unit_exec_vtable, bus_unit_exec_context_find }, { bus_kill_vtable, bus_kill_context_find }), }; @@ -502,6 +525,7 @@ static const BusObjectImplementation bus_swap_object = { { bus_unit_cgroup_vtable, bus_unit_cgroup_find }, { bus_cgroup_vtable, bus_cgroup_context_find }, { bus_exec_vtable, bus_exec_context_find }, + { bus_unit_exec_vtable, bus_unit_exec_context_find }, { bus_kill_vtable, bus_kill_context_find }), }; @@ -1160,6 +1184,7 @@ void dump_bus_properties(FILE *f) { vtable_dump_bus_properties(f, bus_cgroup_vtable); vtable_dump_bus_properties(f, bus_device_vtable); vtable_dump_bus_properties(f, bus_exec_vtable); + vtable_dump_bus_properties(f, bus_unit_exec_vtable); vtable_dump_bus_properties(f, bus_job_vtable); vtable_dump_bus_properties(f, bus_kill_vtable); vtable_dump_bus_properties(f, bus_manager_vtable); diff --git a/src/core/unit.c b/src/core/unit.c index 1bacb56801..aad615383f 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -19,6 +19,7 @@ #include "cgroup-setup.h" #include "cgroup-util.h" #include "chase.h" +#include "chattr-util.h" #include "condition.h" #include "dbus-unit.h" #include "dropin.h" @@ -45,6 +46,7 @@ #include "mountpoint-util.h" #include "path-util.h" #include "process-util.h" +#include "quota-util.h" #include "rm-rf.h" #include "serialize.h" #include "set.h" @@ -6722,6 +6724,52 @@ static uint64_t unit_get_cpu_weight(Unit *u) { return cc ? cgroup_context_cpu_weight(cc, manager_state(u->manager)) : CGROUP_WEIGHT_DEFAULT; } +int unit_get_exec_quota_stats(Unit *u, ExecContext *c, ExecDirectoryType dt, uint64_t *ret_usage, uint64_t *ret_limit) { + int r; + _cleanup_close_ int fd = -EBADF; + _cleanup_free_ char *p = NULL, *pp = NULL; + + assert(u); + assert(c); + + if (c->directories[dt].n_items == 0) { + *ret_usage = UINT64_MAX; + *ret_limit = UINT64_MAX; + return 0; + } + + ExecDirectoryItem *i = &c->directories[dt].items[0]; + p = path_join(u->manager->prefix[dt], i->path); + if (!p) + return log_oom_debug(); + + if (exec_directory_is_private(c, dt)) { + pp = path_join(u->manager->prefix[dt], "private", i->path); + if (!pp) + return log_oom_debug(); + } + + const char *target_dir = pp ?: p; + fd = open(target_dir, O_PATH | O_CLOEXEC | O_DIRECTORY); + if (fd < 0) + return log_unit_debug_errno(u, errno, "Failed to get exec quota stats: %m"); + + uint32_t proj_id; + r = read_fs_xattr_fd(fd, /* ret_xflags = */ NULL, &proj_id); + if (r < 0) + return log_unit_debug_errno(u, r, "Failed to get project ID for exec quota stats: %m"); + + struct dqblk req; + r = quota_query_proj_id(fd, proj_id, &req); + if (r <= 0) + return log_unit_debug_errno(u, r, "Failed to query project ID for exec quota stats: %m"); + + *ret_usage = req.dqb_curspace; + *ret_limit = req.dqb_bhardlimit * QIF_DQBLKSIZE; + + return r; +} + int unit_compare_priority(Unit *a, Unit *b) { int ret; diff --git a/src/core/unit.h b/src/core/unit.h index 4c10514726..6e0b58e62e 100644 --- a/src/core/unit.h +++ b/src/core/unit.h @@ -938,6 +938,8 @@ int unit_add_default_target_dependency(Unit *u, Unit *target); void unit_start_on_termination_deps(Unit *u, UnitDependencyAtom atom); void unit_trigger_notify(Unit *u); +int unit_get_exec_quota_stats(Unit *u, ExecContext *c, ExecDirectoryType dt, uint64_t *ret_usage, uint64_t *ret_limit); + UnitFileState unit_get_unit_file_state(Unit *u); PresetAction unit_get_unit_file_preset(Unit *u); diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c index 41c88b79cf..0e60cd63d4 100644 --- a/src/shared/bus-unit-util.c +++ b/src/shared/bus-unit-util.c @@ -2059,6 +2059,31 @@ static int bus_append_directory(sd_bus_message *m, const char *field, const char return 1; } +static int bus_append_quota_directory(sd_bus_message *m, const char *field, const char *eq) { + uint64_t quota_absolute = UINT64_MAX; + uint32_t quota_scale = UINT32_MAX; + int quota_enforce = false; + int r; + + if (!isempty(eq) && !streq(eq, "off")) { + r = parse_permyriad(eq); + if (r < 0) { + r = parse_size(eq, 1024, "a_absolute); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse argument: %s=%s", field, eq); + } else + quota_scale = UINT32_SCALE_FROM_PERMYRIAD(r); + + quota_enforce = true; + } + + r = sd_bus_message_append(m, "(sv)", field, "(tus)", quota_absolute, quota_scale, yes_no(quota_enforce)); + if (r < 0) + return bus_log_create_error(r); + + return 1; +} + static int bus_append_protect_hostname(sd_bus_message *m, const char *field, const char *eq) { int r; @@ -2514,6 +2539,12 @@ static const BusProperty execute_properties[] = { { "ProtectControlGroupsEx", bus_append_boolean_or_ex_string }, /* compat */ { "PrivateUsers", bus_append_boolean_or_ex_string }, { "PrivateUsersEx", bus_append_boolean_or_ex_string }, /* compat */ + { "StateDirectoryQuota", bus_append_quota_directory }, + { "CacheDirectoryQuota", bus_append_quota_directory }, + { "LogsDirectoryQuota", bus_append_quota_directory }, + { "StateDirectoryAccounting", bus_append_parse_boolean }, + { "CacheDirectoryAccounting", bus_append_parse_boolean }, + { "LogsDirectoryAccounting", bus_append_parse_boolean }, { NULL, bus_try_append_resource_limit, dump_resource_limits }, {} diff --git a/test/fuzz/fuzz-unit-file/directives-all.service b/test/fuzz/fuzz-unit-file/directives-all.service index a0883d0ebe..360ddd3dd9 100644 --- a/test/fuzz/fuzz-unit-file/directives-all.service +++ b/test/fuzz/fuzz-unit-file/directives-all.service @@ -784,6 +784,8 @@ CPUSchedulingPriority= CPUSchedulingResetOnFork= CacheDirectory= CacheDirectoryMode= +CacheDirectoryAccounting= +CacheDirectoryQuota= Capability= Compress= ConfigurationDirectory= @@ -861,6 +863,8 @@ LogRateLimitBurst= LogFilterPatterns= LogsDirectory= LogsDirectoryMode= +LogsDirectoryAccounting= +LogsDirectoryQuota= MACVLAN= MachineID= MaxFileSec= @@ -943,6 +947,8 @@ StandardInputText= StandardOutput= StateDirectory= StateDirectoryMode= +StateDirectoryAccounting= +StateDirectoryQuota= Storage= SuspendKeyIgnoreInhibited= SyncIntervalSec= From e8e274c8da28c75fc1b07dfb17673f0703c2c100 Mon Sep 17 00:00:00 2001 From: Andres Beltran Date: Tue, 1 Jul 2025 17:41:31 +0000 Subject: [PATCH 6/7] Add quota support for systemctl --- src/systemctl/systemctl-show.c | 75 ++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/systemctl/systemctl-show.c b/src/systemctl/systemctl-show.c index e906c08772..e75e7ac1c1 100644 --- a/src/systemctl/systemctl-show.c +++ b/src/systemctl/systemctl-show.c @@ -15,8 +15,10 @@ #include "cgroup-show.h" #include "cpu-set-util.h" #include "errno-util.h" +#include "exec-directory-util.h" #include "exec-util.h" #include "exit-status.h" +#include "extract-word.h" #include "format-util.h" #include "hexdecoct.h" #include "hostname-setup.h" @@ -33,6 +35,7 @@ #include "pager.h" #include "parse-util.h" #include "path-util.h" +#include "percent-util.h" #include "pretty-print.h" #include "process-util.h" #include "set.h" @@ -161,6 +164,13 @@ typedef struct UnitCondition { LIST_FIELDS(struct UnitCondition, conditions); } UnitCondition; +typedef struct QuotaInfo { + bool quota_enforce; + bool quota_accounting; + uint64_t quota_usage; + uint64_t quota_limit; +} QuotaInfo; + static UnitCondition* unit_condition_free(UnitCondition *c) { if (!c) return NULL; @@ -299,6 +309,9 @@ typedef struct UnitStatusInfo { uint64_t default_memory_low; uint64_t default_startup_memory_low; + /* Exec Quotas */ + QuotaInfo exec_directories_quota[_EXEC_DIRECTORY_TYPE_MAX]; + LIST_HEAD(ExecStatusInfo, exec_status_info_list); } UnitStatusInfo; @@ -338,6 +351,22 @@ static void format_enable_state(const char *enable_state, const char **enable_on *enable_on = *enable_off = ""; } +static void print_exec_directory_quota(UnitStatusInfo *i, ExecDirectoryType dt) { + assert(i); + + if (!IN_SET(dt, EXEC_DIRECTORY_STATE, EXEC_DIRECTORY_CACHE, EXEC_DIRECTORY_LOGS)) + return; + + if (i->exec_directories_quota[dt].quota_accounting) { + printf(" %s: %s", exec_directory_type_to_string(dt), FORMAT_BYTES(i->exec_directories_quota[dt].quota_usage)); + + if (i->exec_directories_quota[dt].quota_enforce) + printf(" (max: %s)", FORMAT_BYTES(i->exec_directories_quota[dt].quota_limit)); + + printf("\n"); + } +} + static void print_status_info( sd_bus *bus, UnitStatusInfo *i, @@ -877,6 +906,9 @@ static void print_status_info( if (i->cpu_usage_nsec != UINT64_MAX) printf(" CPU: %s\n", FORMAT_TIMESPAN(i->cpu_usage_nsec / NSEC_PER_USEC, USEC_PER_MSEC)); + for (ExecDirectoryType dt = 0; dt < _EXEC_DIRECTORY_TYPE_MAX; dt++) + print_exec_directory_quota(i, dt); + if (i->control_group) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; static const char prefix[] = " "; @@ -1122,6 +1154,25 @@ static int map_exec(sd_bus *bus, const char *member, sd_bus_message *m, sd_bus_e return 0; } +static int map_quota(sd_bus *bus, const char *member, sd_bus_message *m, sd_bus_error *error, void *userdata) { + int r; + QuotaInfo *qi = ASSERT_PTR(userdata); + uint64_t quota_usage, quota_limit; + + r = sd_bus_message_read(m, "(tt)", "a_usage, "a_limit); + if (r < 0) + return r; + + *qi = (QuotaInfo) { + .quota_enforce = quota_limit != UINT64_MAX, + .quota_accounting = quota_usage != UINT64_MAX, + .quota_usage = quota_usage, + .quota_limit = quota_limit + }; + + return 0; +} + static int print_property(const char *name, const char *expected_value, sd_bus_message *m, BusPrintPropertyFlags flags) { char bus_type; const char *contents; @@ -1338,6 +1389,27 @@ static int print_property(const char *name, const char *expected_value, sd_bus_m fputc('\n', stdout); } return 1; + } else if (STR_IN_SET(name, "StateDirectoryQuota", "CacheDirectoryQuota", "LogsDirectoryQuota")) { + uint64_t quota_absolute; + uint32_t quota_scale; + const char *quota_enforce; + + r = sd_bus_message_read(m, "(tus)", "a_absolute, "a_scale, "a_enforce); + if (r < 0) + return r; + + r = parse_boolean(quota_enforce); + if (r < 0) + return r; + + if (!r) + bus_print_property_value(name, expected_value, flags, "[not set]"); + else if (quota_absolute != UINT64_MAX) + bus_print_property_valuef(name, expected_value, flags, "%" PRIu64, quota_absolute); + else + bus_print_property_valuef(name, expected_value, flags, "%d%%", UINT32_SCALE_TO_PERCENT(quota_scale)); + + return 1; } break; @@ -2201,6 +2273,9 @@ static int show_one( { "IPEgressBytes", "t", NULL, offsetof(UnitStatusInfo, ip_egress_bytes) }, { "IOReadBytes", "t", NULL, offsetof(UnitStatusInfo, io_read_bytes) }, { "IOWriteBytes", "t", NULL, offsetof(UnitStatusInfo, io_write_bytes) }, + { "StateDirectoryQuotaUsage", "(tt)", map_quota, offsetof(UnitStatusInfo, exec_directories_quota[EXEC_DIRECTORY_STATE]) }, + { "CacheDirectoryQuotaUsage", "(tt)", map_quota, offsetof(UnitStatusInfo, exec_directories_quota[EXEC_DIRECTORY_CACHE]) }, + { "LogsDirectoryQuotaUsage", "(tt)", map_quota, offsetof(UnitStatusInfo, exec_directories_quota[EXEC_DIRECTORY_LOGS]) }, { "ExecCondition", "a(sasbttttuii)", map_exec, 0 }, { "ExecConditionEx", "a(sasasttttuii)", map_exec, 0 }, { "ExecStartPre", "a(sasbttttuii)", map_exec, 0 }, From c777c05125c0dc7cc07206ea130079c33f92602f Mon Sep 17 00:00:00 2001 From: Andres Beltran Date: Mon, 16 Jun 2025 23:44:35 +0000 Subject: [PATCH 7/7] test: add test for quotas on Exec directories --- mkosi/mkosi.repart/20-disk.conf | 5 ++ test/units/TEST-07-PID1.quota.sh | 95 ++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 mkosi/mkosi.repart/20-disk.conf create mode 100755 test/units/TEST-07-PID1.quota.sh diff --git a/mkosi/mkosi.repart/20-disk.conf b/mkosi/mkosi.repart/20-disk.conf new file mode 100644 index 0000000000..0090dc00da --- /dev/null +++ b/mkosi/mkosi.repart/20-disk.conf @@ -0,0 +1,5 @@ +[Partition] +Type=linux-generic +Format=ext4 +SizeMinBytes=498M +SizeMaxBytes=498M diff --git a/test/units/TEST-07-PID1.quota.sh b/test/units/TEST-07-PID1.quota.sh new file mode 100755 index 0000000000..748b5ee658 --- /dev/null +++ b/test/units/TEST-07-PID1.quota.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +set -eux +set -o pipefail + +systemd-analyze log-level debug + +test_quotas() { + + local directory="$1" + local exec_directory_directive="$2" + local exec_quota_directive="$3" + local mountpoint="/datadrive" + + dev_num=$(lsblk | grep "498M" | awk '{print $1}' | sed 's/[^a-zA-Z0-9]*//g') + + if ! tune2fs -Q prjquota "/dev/${dev_num}"; then + return + fi + mkdir -p "${mountpoint}" + mount "/dev/${dev_num}" "${mountpoint}" + + mv /var/lib/ "${mountpoint}" + rm -rf /var/lib/ && ln -s "${mountpoint}/lib/" /var/ + + rm -rf "${directory}/quotadir" + + cat >/run/systemd/system/testservice-07-check-quotas.service </run/systemd/system/testservice-07-check-quotas.service <