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/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/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
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/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/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/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);
diff --git a/src/core/unit.c b/src/core/unit.c
index 9ef8ca2ed0..ff6bac576d 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/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',
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);
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 },
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=
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 <