From: Timo Sirainen Date: Tue, 5 May 2026 16:18:34 +0000 (+0000) Subject: [PATCH 1/7] lib-sieve: Add per-user sieve-rusage file for cumulative CPU tracking X-Git-Tag: archive/raspbian/1%2.4.1+dfsg1-6+rpi1+deb13u7^2~114 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=38b546cae160ba22111eff3fe4745fb839edacf0;p=dovecot.git [PATCH 1/7] lib-sieve: Add per-user sieve-rusage file for cumulative CPU tracking 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 /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 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 --- diff --git a/pigeonhole/src/lib-sieve/Makefile.am b/pigeonhole/src/lib-sieve/Makefile.am index 0559879..ae41ce5 100644 --- a/pigeonhole/src/lib-sieve/Makefile.am +++ b/pigeonhole/src/lib-sieve/Makefile.am @@ -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 \ diff --git a/pigeonhole/src/lib-sieve/sieve-binary-file.c b/pigeonhole/src/lib-sieve/sieve-binary-file.c index aa1fabf..5b06ab9 100644 --- a/pigeonhole/src/lib-sieve/sieve-binary-file.c +++ b/pigeonhole/src/lib-sieve/sieve-binary-file.c @@ -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) { diff --git a/pigeonhole/src/lib-sieve/sieve-binary.c b/pigeonhole/src/lib-sieve/sieve-binary.c index 4524757..cb24833 100644 --- a/pigeonhole/src/lib-sieve/sieve-binary.c +++ b/pigeonhole/src/lib-sieve/sieve-binary.c @@ -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) { diff --git a/pigeonhole/src/lib-sieve/sieve-binary.h b/pigeonhole/src/lib-sieve/sieve-binary.h index 227a1c4..f8bc596 100644 --- a/pigeonhole/src/lib-sieve/sieve-binary.h +++ b/pigeonhole/src/lib-sieve/sieve-binary.h @@ -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 index 0000000..9e01cc4 --- /dev/null +++ b/pigeonhole/src/lib-sieve/sieve-rusage.c @@ -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 +#include +#include +#include + +#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 index 0000000..823ded3 --- /dev/null +++ b/pigeonhole/src/lib-sieve/sieve-rusage.h @@ -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 /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 diff --git a/pigeonhole/src/lib-sieve/sieve-storage-private.h b/pigeonhole/src/lib-sieve/sieve-storage-private.h index 1bccdca..f2d728a 100644 --- a/pigeonhole/src/lib-sieve/sieve-storage-private.h +++ b/pigeonhole/src/lib-sieve/sieve-storage-private.h @@ -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; diff --git a/pigeonhole/src/lib-sieve/sieve-storage.c b/pigeonhole/src/lib-sieve/sieve-storage.c index ef781a1..3f33a41 100644 --- a/pigeonhole/src/lib-sieve/sieve-storage.c +++ b/pigeonhole/src/lib-sieve/sieve-storage.c @@ -17,6 +17,7 @@ #include "sieve-script-private.h" #include "sieve-storage-private.h" +#include "sieve-rusage.h" #include #include @@ -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) { diff --git a/pigeonhole/src/lib-sieve/sieve.c b/pigeonhole/src/lib-sieve/sieve.c index 0566275..4a1282f 100644 --- a/pigeonhole/src/lib-sieve/sieve.c +++ b/pigeonhole/src/lib-sieve/sieve.c @@ -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. */