]> dgit.raspbian.org Git - dovecot.git/commitdiff
[PATCH 1/7] lib-sieve: Add per-user sieve-rusage file for cumulative CPU tracking
authorTimo Sirainen <timo.sirainen@open-xchange.com>
Tue, 5 May 2026 16:18:34 +0000 (16:18 +0000)
committerNoah Meyerhans <noahm@debian.org>
Wed, 16 Sep 2026 19:06:35 +0000 (15:06 -0400)
Cumulative resource usage was previously stored in each compiled Sieve
binary's (.svbin) header. Because the binary file is keyed by script
name, a user could reset their CPU budget by uploading the same script
under a different name (PUTSCRIPT newname + SETACTIVE newname),
RENAMESCRIPT, or DELETESCRIPT followed by PUTSCRIPT under another
name. Each such operation produced a fresh .svbin with zero counter,
allowing the sieve_max_cpu_time limit to be bypassed indefinitely.

Track cumulative CPU per user instead, in <INBOX-namespace-root>/sieve-rusage.
The file persists across all script renames, deletions, and re-uploads,
since it is not tied to any particular script name. Tracking is enabled
only for personal storages where the user's INBOX namespace has a
filesystem path; admin/default storages are unaffected here.

The on-disk format is a single ASCII line:

    V1 <flags> <cpu-secs> <update-time>

CPU time is rounded up to whole seconds when persisted; sub-second
precision is preserved only for the in-memory accumulation within a
single execution. The file is updated by writing a new copy via
file_dotlock_open_mode() and replacing the original via
file_dotlock_replace(). Concurrent writers are serialized through the
dotlock; readers do not need a lock because the rename is atomic.

The .svbin header still carries its original resource_usage struct as
the active fallback when no per-user file is available; a follow-up
commit drops that path.

Gbp-Pq: Name 0001-lib-sieve-Add-per-user-sieve-rusage-file-for-cumulat.patch

pigeonhole/src/lib-sieve/Makefile.am
pigeonhole/src/lib-sieve/sieve-binary-file.c
pigeonhole/src/lib-sieve/sieve-binary.c
pigeonhole/src/lib-sieve/sieve-binary.h
pigeonhole/src/lib-sieve/sieve-rusage.c [new file with mode: 0644]
pigeonhole/src/lib-sieve/sieve-rusage.h [new file with mode: 0644]
pigeonhole/src/lib-sieve/sieve-storage-private.h
pigeonhole/src/lib-sieve/sieve-storage.c
pigeonhole/src/lib-sieve/sieve.c

index 0559879995f8f76394570781ae947ea9d2352337..ae41ce53c5d826bf122aa1765d933fe670d35aed 100644 (file)
@@ -109,6 +109,7 @@ libdovecot_sieve_la_SOURCES = \
        sieve-ast.c \
        sieve-binary.c \
        sieve-binary-file.c \
+       sieve-rusage.c \
        sieve-binary-code.c \
        sieve-binary-debug.c \
        sieve-parser.c \
@@ -158,6 +159,7 @@ headers = \
        sieve-ast.h \
        sieve-binary.h \
        sieve-binary-private.h \
+       sieve-rusage.h \
        sieve-parser.h \
        sieve-address.h \
        sieve-validator.h \
index aa1fabf33f2dec1b8d8037fe0e946d7b42581579..5b06ab9020ecbffd6f4b030f3db8d63fb1a3e192 100644 (file)
@@ -18,6 +18,9 @@
 #include "sieve-extensions.h"
 #include "sieve-code.h"
 #include "sieve-script.h"
+#include "sieve-script-private.h"
+#include "sieve-storage-private.h"
+#include "sieve-rusage.h"
 
 #include "sieve-binary-private.h"
 
@@ -1008,15 +1011,56 @@ sieve_binary_file_do_update_resource_usage(
        return ret;
 }
 
+static int
+sieve_binary_storage_update_resource_usage(struct sieve_binary *sbin,
+                                          struct sieve_storage *storage,
+                                          enum sieve_error *error_code_r)
+{
+       struct sieve_resource_usage delta;
+       struct sieve_resource_usage total;
+       uint32_t flags;
+
+       delta = sbin->rusage;
+       sieve_binary_get_resource_usage(sbin, &total);
+       flags = sbin->header.flags & SIEVE_BINARY_FLAG_RESOURCE_LIMIT;
+
+       if (!HAS_ALL_BITS(flags, SIEVE_BINARY_FLAG_RESOURCE_LIMIT) &&
+           !sieve_resource_usage_is_high(sbin->svinst, &total)) {
+               /* Nothing meaningful to persist */
+               sieve_resource_usage_init(&sbin->rusage);
+               sbin->rusage_updated = FALSE;
+               return 0;
+       }
+
+       if (sieve_rusage_storage_add(storage, &delta, flags) < 0) {
+               *error_code_r = SIEVE_ERROR_TEMP_FAILURE;
+               return -1;
+       }
+
+       sieve_resource_usage_init(&sbin->rusage);
+       sbin->rusage_updated = FALSE;
+       return 0;
+}
+
 int sieve_binary_file_update_resource_usage(struct sieve_binary *sbin,
                                            enum sieve_error *error_code_r)
 {
+       struct sieve_storage *storage = NULL;
        int fd, ret = 0;
 
        sieve_error_args_init(&error_code_r, NULL);
 
        sieve_binary_file_close(&sbin->file);
 
+       if (sbin->script != NULL && sbin->script->storage != NULL &&
+           sbin->script->storage->rusage_path != NULL)
+               storage = sbin->script->storage;
+
+       if (storage != NULL) {
+               return sieve_binary_storage_update_resource_usage(
+                       sbin, storage, error_code_r);
+       }
+
        if (sbin->path == NULL)
                return 0;
        if (sbin->header.version_major != SIEVE_BINARY_VERSION_MAJOR) {
index 45247579ac2da8180a56d14e6dcc20b4a3ab35e3..cb248338d7b0108be8f0945fbad240966d40df13 100644 (file)
@@ -9,6 +9,7 @@
 #include "hash.h"
 #include "array.h"
 #include "ostream.h"
+#include "ioloop.h"
 #include "eacces-error.h"
 #include "safe-mkstemp.h"
 
@@ -16,6 +17,9 @@
 #include "sieve-extensions.h"
 #include "sieve-code.h"
 #include "sieve-script.h"
+#include "sieve-script-private.h"
+#include "sieve-storage-private.h"
+#include "sieve-rusage.h"
 
 #include "sieve-binary-private.h"
 
@@ -190,6 +194,37 @@ void sieve_binary_close(struct sieve_binary **_sbin)
  * Resource usage
  */
 
+static struct sieve_storage *
+sieve_binary_get_user_storage(struct sieve_binary *sbin)
+{
+       if (sbin->script == NULL)
+               return NULL;
+       if (sbin->script->storage == NULL)
+               return NULL;
+       if (sbin->script->storage->rusage_path == NULL)
+               return NULL;
+       return sbin->script->storage;
+}
+
+void sieve_binary_apply_persisted_rusage(struct sieve_binary *sbin)
+{
+       struct sieve_storage *storage = sieve_binary_get_user_storage(sbin);
+       struct sieve_resource_usage rusage;
+       struct sieve_binary_header *header = &sbin->header;
+       uint32_t flags = 0;
+
+       if (storage == NULL)
+               return;
+       if (sieve_rusage_storage_load(storage, &rusage, &flags) <= 0)
+               return;
+
+       header->resource_usage.cpu_time_msecs = rusage.cpu_time_msecs;
+       header->resource_usage.update_time = ioloop_time;
+       header->flags |= (flags & SIEVE_BINARY_FLAG_RESOURCE_LIMIT);
+       sieve_resource_usage_init(&sbin->rusage);
+       sbin->rusage_updated = FALSE;
+}
+
 void sieve_binary_get_resource_usage(struct sieve_binary *sbin,
                                     struct sieve_resource_usage *rusage_r)
 {
index 227a1c46467bc79ca201f7d40b1a9a459fe84c52..f8bc59680676e16e58b2a7402ebe35a3646f470d 100644 (file)
@@ -37,6 +37,11 @@ bool sieve_binary_record_resource_usage(
        ATTR_NULL(1);
 void sieve_binary_set_resource_usage(struct sieve_binary *sbin,
                                     const struct sieve_resource_usage *rusage);
+
+/* Apply persisted per-user rusage and limit flag from sieve-rusage file
+   into the binary header. No-op when the storage has no per-user file. */
+void sieve_binary_apply_persisted_rusage(struct sieve_binary *sbin);
+
 /*
  * Accessors
  */
diff --git a/pigeonhole/src/lib-sieve/sieve-rusage.c b/pigeonhole/src/lib-sieve/sieve-rusage.c
new file mode 100644 (file)
index 0000000..9e01cc4
--- /dev/null
@@ -0,0 +1,266 @@
+/* Copyright (c) 2026 Pigeonhole authors, see the included COPYING file
+ */
+
+#include "lib.h"
+#include "str.h"
+#include "strnum.h"
+#include "ioloop.h"
+#include "eacces-error.h"
+#include "file-dotlock.h"
+#include "mail-namespace.h"
+#include "mail-storage.h"
+#include "mailbox-list.h"
+
+#include "sieve-common.h"
+#include "sieve-settings.h"
+#include "sieve-storage-private.h"
+#include "sieve-rusage.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#define SIEVE_RUSAGE_FILENAME "sieve-rusage"
+#define SIEVE_RUSAGE_VERSION_TAG "V1"
+#define SIEVE_RUSAGE_LOCK_TIMEOUT 10
+#define SIEVE_RUSAGE_LOCK_STALE_TIMEOUT 60
+
+/* Maximum size of the on-disk file. Single-line ASCII format with four
+   space-separated tokens; anything larger is treated as corrupt. */
+#define SIEVE_RUSAGE_MAX_FILE_SIZE 128
+
+struct sieve_rusage_record {
+       uint32_t flags;
+       uint32_t cpu_time_msecs;
+       time_t update_time;
+};
+
+void sieve_rusage_storage_init(struct sieve_storage *storage,
+                              struct mail_user *user)
+{
+       struct mail_namespace *ns;
+       const char *root;
+
+       if (!storage->is_personal || user == NULL)
+               return;
+
+       ns = mail_namespace_find_inbox(user->namespaces);
+       if (ns == NULL)
+               return;
+
+       if (!mailbox_list_get_root_path(ns->list, MAILBOX_LIST_PATH_TYPE_DIR,
+                                       &root) || root == NULL)
+               return;
+
+       storage->user = user;
+       storage->inbox_list = ns->list;
+       storage->rusage_path = p_strconcat(storage->pool, root, "/",
+                                          SIEVE_RUSAGE_FILENAME, NULL);
+}
+
+static bool
+sieve_rusage_parse(const char *line, struct sieve_rusage_record *rec)
+{
+       const char *const *tokens;
+       uint32_t cpu_secs;
+       bool ok = FALSE;
+
+       T_BEGIN {
+               tokens = t_strsplit_spaces(line, " \t");
+               if (tokens[0] == NULL ||
+                   strcmp(tokens[0], SIEVE_RUSAGE_VERSION_TAG) != 0)
+                       break;
+               if (tokens[1] == NULL || tokens[2] == NULL ||
+                   tokens[3] == NULL || tokens[4] != NULL)
+                       break;
+               if (str_to_uint32(tokens[1], &rec->flags) < 0)
+                       break;
+               if (str_to_uint32(tokens[2], &cpu_secs) < 0)
+                       break;
+               if (str_to_time(tokens[3], &rec->update_time) < 0)
+                       break;
+               if (cpu_secs > UINT32_MAX / 1000)
+                       rec->cpu_time_msecs = UINT32_MAX;
+               else
+                       rec->cpu_time_msecs = cpu_secs * 1000;
+               ok = TRUE;
+       } T_END;
+
+       return ok;
+}
+
+static int
+sieve_rusage_read(struct sieve_storage *storage,
+                 struct sieve_rusage_record *rec_r)
+{
+       char buf[SIEVE_RUSAGE_MAX_FILE_SIZE + 1];
+       char *nl;
+       ssize_t ret;
+       int fd;
+
+       i_zero(rec_r);
+
+       fd = open(storage->rusage_path, O_RDONLY);
+       if (fd < 0) {
+               if (errno == ENOENT)
+                       return 0;
+               e_error(storage->event, "open(%s) failed: %m",
+                       storage->rusage_path);
+               return -1;
+       }
+
+       ret = read(fd, buf, sizeof(buf) - 1);
+       i_close_fd(&fd);
+       if (ret < 0) {
+               e_error(storage->event, "read(%s) failed: %m",
+                       storage->rusage_path);
+               return -1;
+       }
+       if (ret == 0)
+               return 0;
+       if (ret >= (ssize_t)(sizeof(buf) - 1)) {
+               e_warning(storage->event, "%s: file too large; resetting",
+                         storage->rusage_path);
+               return 0;
+       }
+       buf[ret] = '\0';
+
+       nl = strchr(buf, '\n');
+       if (nl != NULL)
+               *nl = '\0';
+
+       if (!sieve_rusage_parse(buf, rec_r)) {
+               e_warning(storage->event, "%s: malformed content; resetting",
+                         storage->rusage_path);
+               i_zero(rec_r);
+       }
+       return 0;
+}
+
+static void
+sieve_rusage_dotlock_settings(struct dotlock_settings *set_r)
+{
+       i_zero(set_r);
+       set_r->timeout = SIEVE_RUSAGE_LOCK_TIMEOUT;
+       set_r->stale_timeout = SIEVE_RUSAGE_LOCK_STALE_TIMEOUT;
+       set_r->use_excl_lock = TRUE;
+}
+
+int sieve_rusage_storage_load(struct sieve_storage *storage,
+                             struct sieve_resource_usage *rusage_r,
+                             uint32_t *flags_r)
+{
+       struct sieve_rusage_record rec;
+       unsigned int timeout;
+
+       sieve_resource_usage_init(rusage_r);
+       *flags_r = 0;
+
+       if (storage->rusage_path == NULL)
+               return 0;
+
+       if (sieve_rusage_read(storage, &rec) < 0)
+               return -1;
+
+       timeout = storage->svinst->set->resource_usage_timeout;
+       if (rec.update_time != 0 &&
+           (ioloop_time - rec.update_time) > (time_t)timeout) {
+               /* decayed */
+               return 1;
+       }
+
+       rusage_r->cpu_time_msecs = rec.cpu_time_msecs;
+       *flags_r = rec.flags;
+       return 1;
+}
+
+int sieve_rusage_storage_add(struct sieve_storage *storage,
+                            const struct sieve_resource_usage *delta_rusage,
+                            uint32_t flags_to_set)
+{
+       struct sieve_rusage_record rec;
+       struct dotlock_settings dlset;
+       struct dotlock *dotlock;
+       unsigned int timeout;
+       uint32_t old_cpu, cpu_secs;
+       string_t *out;
+       ssize_t wret;
+       int fd;
+
+       if (storage->rusage_path == NULL)
+               return 0;
+
+       sieve_rusage_dotlock_settings(&dlset);
+
+       /* The namespace root may not exist yet on the very first update for
+          a user (e.g. LDA refused execution before any mail was delivered).
+          Create it on demand so file_dotlock_open does not fail with ENOENT. */
+       if (storage->inbox_list != NULL &&
+           mailbox_list_mkdir_root(storage->inbox_list, NULL,
+                                   MAILBOX_LIST_PATH_TYPE_DIR) < 0)
+               return -1;
+
+       fd = file_dotlock_open_mode(&dlset, storage->rusage_path, 0,
+                                   0600, (uid_t)-1, (gid_t)-1, &dotlock);
+       if (fd == -1) {
+               if (errno == EACCES) {
+                       e_error(storage->event, "%s",
+                               eacces_error_get_creating("file_dotlock_open",
+                                                         storage->rusage_path));
+               } else {
+                       e_error(storage->event,
+                               "file_dotlock_open(%s) failed: %m",
+                               storage->rusage_path);
+               }
+               return -1;
+       }
+
+       if (sieve_rusage_read(storage, &rec) < 0) {
+               file_dotlock_delete(&dotlock);
+               return -1;
+       }
+
+       timeout = storage->svinst->set->resource_usage_timeout;
+       if (rec.update_time != 0 &&
+           (ioloop_time - rec.update_time) > (time_t)timeout) {
+               /* decayed: discard previous values */
+               rec.cpu_time_msecs = 0;
+               rec.flags = 0;
+       }
+
+       old_cpu = rec.cpu_time_msecs;
+       if ((UINT32_MAX - old_cpu) < delta_rusage->cpu_time_msecs)
+               rec.cpu_time_msecs = UINT32_MAX;
+       else
+               rec.cpu_time_msecs = old_cpu + delta_rusage->cpu_time_msecs;
+       rec.flags |= flags_to_set;
+       rec.update_time = ioloop_time;
+
+       /* Round CPU time up to whole seconds for persistence. */
+       if (rec.cpu_time_msecs >= UINT32_MAX - 999)
+               cpu_secs = UINT32_MAX / 1000;
+       else
+               cpu_secs = (rec.cpu_time_msecs + 999) / 1000;
+
+       out = t_str_new(64);
+       str_printfa(out, "%s %u %u %lld\n", SIEVE_RUSAGE_VERSION_TAG,
+                   rec.flags, cpu_secs, (long long)rec.update_time);
+
+       wret = write(fd, str_data(out), str_len(out));
+       if (wret != (ssize_t)str_len(out)) {
+               e_error(storage->event,
+                       "write(%s) failed: %m (wrote %zd/%zu)",
+                       storage->rusage_path, wret, str_len(out));
+               file_dotlock_delete(&dotlock);
+               return -1;
+       }
+
+       if (file_dotlock_replace(&dotlock, 0) < 0) {
+               e_error(storage->event,
+                       "file_dotlock_replace(%s) failed: %m",
+                       storage->rusage_path);
+               return -1;
+       }
+       return 1;
+}
diff --git a/pigeonhole/src/lib-sieve/sieve-rusage.h b/pigeonhole/src/lib-sieve/sieve-rusage.h
new file mode 100644 (file)
index 0000000..823ded3
--- /dev/null
@@ -0,0 +1,32 @@
+#ifndef SIEVE_RUSAGE_H
+#define SIEVE_RUSAGE_H
+
+#include "sieve-common.h"
+
+struct sieve_storage;
+struct mail_user;
+
+/* Per-user resource usage file lives at <INBOX-namespace-root>/sieve-rusage.
+   It survives script renames, deletions, and re-uploads under a different
+   name, preventing CPU-budget bypass. Tracking is enabled only for personal
+   user-controlled storage. */
+
+void sieve_rusage_storage_init(struct sieve_storage *storage,
+                              struct mail_user *user);
+
+/* Load persisted resource usage from the per-user file. Returns 1 on success,
+   0 if the storage has no rusage file (no INBOX namespace path), -1 on error.
+   Applies the resource_usage_timeout decay. flags_r receives sieve binary
+   header flags (RESOURCE_LIMIT) that were persisted. */
+int sieve_rusage_storage_load(struct sieve_storage *storage,
+                             struct sieve_resource_usage *rusage_r,
+                             uint32_t *flags_r);
+
+/* Atomically add delta_rusage to and OR flags_to_set into the persisted
+   per-user rusage file. Read-modify-write under fcntl WRLCK. Returns 1 on
+   success, 0 if no rusage file is configured, -1 on error. */
+int sieve_rusage_storage_add(struct sieve_storage *storage,
+                            const struct sieve_resource_usage *delta_rusage,
+                            uint32_t flags_to_set);
+
+#endif
index 1bccdcab8171a59ca00999b0b7e24647922cfb18..f2d728aa053c3cd415e391c47a1fb0f143845da1 100644 (file)
@@ -118,6 +118,17 @@ struct sieve_storage {
 
        struct mail_namespace *sync_inbox_ns;
 
+       /* Per-user resource usage tracking. Set when storage is personal and
+          the user's INBOX namespace has a filesystem path. NULL otherwise.
+          inbox_list is kept so the namespace root directory can be created
+          on demand before the rusage file is updated; LDA may close the
+          sieve binary (and therefore persist rusage) before any mail has
+          been delivered, in which case the namespace root may not exist
+          yet. */
+       struct mail_user *user;
+       struct mailbox_list *inbox_list;
+       const char *rusage_path;
+
        enum sieve_storage_flags flags;
 
        bool allows_synchronization:1;
index ef781a16004560021cd8411f834414d60ecb21b9..3f33a418c6f2e50300e3a43976455e5e989112d4 100644 (file)
@@ -17,6 +17,7 @@
 
 #include "sieve-script-private.h"
 #include "sieve-storage-private.h"
+#include "sieve-rusage.h"
 
 #include <sys/types.h>
 #include <sys/stat.h>
@@ -777,6 +778,7 @@ int sieve_storage_create_personal(struct sieve_instance *svinst,
        if (ret == 0) {
                i_assert(storage->is_personal);
                (void)sieve_storage_sync_init(storage, user);
+               sieve_rusage_storage_init(storage, user);
        } else if (*error_code_r != SIEVE_ERROR_TEMP_FAILURE &&
                   (flags & SIEVE_STORAGE_FLAG_SYNCHRONIZING) == 0 &&
                   (flags & SIEVE_STORAGE_FLAG_READWRITE) == 0) {
index 0566275e43c87d885d7e08a614c80afe92edd159..4a1282f695174aa6846cdbdaf73dea3164bcfe9f 100644 (file)
@@ -30,6 +30,7 @@
 #include "sieve-generator.h"
 #include "sieve-interpreter.h"
 #include "sieve-binary-dumper.h"
+#include "sieve-rusage.h"
 
 #include "sieve.h"
 #include "sieve-common.h"
@@ -444,6 +445,8 @@ sieve_open_script_real(struct sieve_script *script,
 
        /* Try to open the matching binary */
        if (sieve_script_binary_load(script, &sbin, error_code_r) == 0) {
+               /* Per-user rusage file (if any) overrides binary header */
+               sieve_binary_apply_persisted_rusage(sbin);
                sieve_binary_get_resource_usage(sbin, &rusage);
 
                /* Ok, it exists; now let's see if it is up to date */
@@ -474,6 +477,8 @@ sieve_open_script_real(struct sieve_script *script,
                        sieve_script_label(script));
 
                sieve_binary_set_resource_usage(sbin, &rusage);
+               /* Per-user rusage file (if any) overrides any carry-forward */
+               sieve_binary_apply_persisted_rusage(sbin);
        }
 
        /* Check whether binary can be executed. */