Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package postsrsd for openSUSE:Factory checked in at 2026-08-14 22:06:58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/postsrsd (Old) and /work/SRC/openSUSE:Factory/.postsrsd.new.1258 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "postsrsd" Fri Aug 14 22:06:58 2026 rev:23 rq:1371023 version:2.4.0 Changes: -------- --- /work/SRC/openSUSE:Factory/postsrsd/postsrsd.changes 2026-07-17 20:38:24.962354071 +0200 +++ /work/SRC/openSUSE:Factory/.postsrsd.new.1258/postsrsd.changes 2026-08-14 22:07:22.707873101 +0200 @@ -1,0 +2,9 @@ +Thu Aug 13 10:29:34 UTC 2026 - Jan Engelhardt <[email protected]> + +- Update to release 2.4.0 + * New configuration option ``connection-limit`` to prevent DoS + attacks by malicious clients repeatedly connecting to PostSRSd. + * New configuration option ``milter-recipient-limit`` to keep the + number of recipients per SMTP session sane. + +------------------------------------------------------------------- Old: ---- 2.3.0.tar.gz New: ---- 2.4.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ postsrsd.spec ++++++ --- /var/tmp/diff_new_pack.81HPbR/_old 2026-08-14 22:07:23.836914208 +0200 +++ /var/tmp/diff_new_pack.81HPbR/_new 2026-08-14 22:07:23.838914281 +0200 @@ -17,7 +17,7 @@ Name: postsrsd -Version: 2.3.0 +Version: 2.4.0 Release: 0 Summary: Sender Rewriting Support for postfix License: GPL-2.0-only ++++++ 2.3.0.tar.gz -> 2.4.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/.pre-commit-config.yaml new/postsrsd-2.4.0/.pre-commit-config.yaml --- old/postsrsd-2.3.0/.pre-commit-config.yaml 2026-07-17 19:04:57.000000000 +0200 +++ new/postsrsd-2.4.0/.pre-commit-config.yaml 2026-08-12 10:28:49.000000000 +0200 @@ -5,8 +5,11 @@ rev: v6.0.0 hooks: - id: trailing-whitespace + exclude: ^fuzz/(?:seeds|dict) - id: end-of-file-fixer + exclude: ^fuzz/(?:seeds|dict) - id: check-yaml + exclude: ^fuzz/(?:seeds|dict) - id: check-added-large-files - repo: https://github.com/pre-commit/mirrors-clang-format rev: v22.1.8 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/CHANGELOG.rst new/postsrsd-2.4.0/CHANGELOG.rst --- old/postsrsd-2.3.0/CHANGELOG.rst 2026-07-17 19:04:57.000000000 +0200 +++ new/postsrsd-2.4.0/CHANGELOG.rst 2026-08-12 10:28:49.000000000 +0200 @@ -7,6 +7,27 @@ Changelog ######### +2.4.0 +===== + +Fixed +----- + +* A few minor code defects have been fixed to contribute to defense in depth. + +Added +----- + +* New configuration option ``connection-limit`` to prevent DoS attacks by + malicious clients repeatedly connecting to PostSRSd. +* New configuration option ``milter-recipient-limit`` to keep the number + of recipients per SMTP session sane. + +Changed +------- + +* The milter protocol implementation is no longer considered experimental. + 2.3.0 ===== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/CMakeLists.txt new/postsrsd-2.4.0/CMakeLists.txt --- old/postsrsd-2.3.0/CMakeLists.txt 2026-07-17 19:04:57.000000000 +0200 +++ new/postsrsd-2.4.0/CMakeLists.txt 2026-08-12 10:28:49.000000000 +0200 @@ -17,7 +17,7 @@ cmake_minimum_required(VERSION 3.24...4.4) project( postsrsd - VERSION 2.3.0 + VERSION 2.4.0 LANGUAGES C DESCRIPTION "Sender Rewriting Scheme daemon for Postfix" HOMEPAGE_URL "https://github.com/roehling/postsrsd" @@ -54,6 +54,8 @@ CACHE STRING "The default unprivileged user as which PostSRSd will run" ) +option(BUILD_FUZZING "Build fuzzing harnesses" OFF) + option(WITH_SQLITE "Enable sqlite-based storage for opaque SRS tokens (requires sqlite3)" OFF @@ -354,12 +356,6 @@ if(DEVELOPER_BUILD AND CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") add_compile_options(-Wall -Wextra -Werror) - if(CMAKE_C_COMPILER_ID STREQUAL "GNU" AND CMAKE_C_COMPILER_VERSION - VERSION_LESS 11.0 - ) - # isspace() / isalnum() / tolower() / toupper() trigger this warning - add_compile_options(-Wno-char-subscripts) - endif() endif() add_executable( @@ -507,6 +503,10 @@ add_subdirectory(tests) endif() +if(BUILD_FUZZING) + add_subdirectory(fuzz) +endif() + feature_summary(WHAT ENABLED_FEATURES DISABLED_FEATURES) if(DEFINED INIT_FLAVOR) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/README.rst new/postsrsd-2.4.0/README.rst --- old/postsrsd-2.3.0/README.rst 2026-07-17 19:04:57.000000000 +0200 +++ new/postsrsd-2.4.0/README.rst 2026-08-12 10:28:49.000000000 +0200 @@ -145,20 +145,15 @@ .. _example: doc/postsrsd.conf -Experimental Milter Support -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Milter Support +~~~~~~~~~~~~~~ -PostSRSd 2.x has added optional support for the Milter protocol. You can set -the ``milter`` option in ``postsrsd.conf`` and add the corresponding line to -your ``etc/postfix/main.cf``:: +PostSRSd 2.x has added support for the Milter protocol. You can set the +``milter`` option in ``postsrsd.conf`` and add the corresponding line to your +``etc/postfix/main.cf``:: smtpd_milters = unix:srs_milter -Note that the Milter code is less tested and should be considered experimental -for now and not ready for production. Feel free to report bugs or open pull -requests if you try it out, though. Until PostSRSd 2.1, the milter support was -implemented with the external libMilter library from Sendmail. - Inotify Support ~~~~~~~~~~~~~~~ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/data/postsrsd.conf.in new/postsrsd-2.4.0/data/postsrsd.conf.in --- old/postsrsd-2.3.0/data/postsrsd.conf.in 2026-07-17 19:04:57.000000000 +0200 +++ new/postsrsd-2.4.0/data/postsrsd.conf.in 2026-08-12 10:28:49.000000000 +0200 @@ -83,6 +83,17 @@ # #keep-alive = 30 +# Connection limit. +# This is a hard limit for the number of concurrent connections. It is +# a fail-safe to prevent PostSRSd from forking an unbounded number of +# child processes. Once this limit is reached, PostSRSd will accept but +# immediately close incoming connections without serving requests. +# +# Default: +# connection-limit = 200 +# +#connection-limit = 200 + # Secret keys for signing and verifying SRS addresses. # Rewritten addresses are tagged with a truncated HMAC-SHA1 signature, to # prevent tampering and forged envelope addresses. You can have more than @@ -165,6 +176,16 @@ # #milter-rewrite-local = off +# Recipient limit per milter transaction +# PostSRSd rejects emails which exceed this recipient limit, to prevent +# broken or malicious MTAs from consuming arbitrary amounts of memory. The +# default limit matches the Postfix default limit for SMTP transactions. +# +# Default: +# milter-recipient-limit = 1000 +# +#milter-recipient-limit = 1000 + # SRS tag separator # This is the character following the initial SRS0 or SRS1 tag of a generated # sender address. Valid separators are "=", "+", and "-". Unless you have a diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/doc/postsrsd.conf new/postsrsd-2.4.0/doc/postsrsd.conf --- old/postsrsd-2.3.0/doc/postsrsd.conf 2026-07-17 19:04:57.000000000 +0200 +++ new/postsrsd-2.4.0/doc/postsrsd.conf 2026-08-12 10:28:49.000000000 +0200 @@ -83,6 +83,17 @@ # #keep-alive = 30 +# Connection limit. +# This is a hard limit for the number of concurrent connections. It is +# a fail-safe to prevent PostSRSd from forking an unbounded number of +# child processes. Once this limit is reached, PostSRSd will accept but +# immediately close incoming connections without serving requests. +# +# Default: +# connection-limit = 200 +# +#connection-limit = 200 + # Secret keys for signing and verifying SRS addresses. # Rewritten addresses are tagged with a truncated HMAC-SHA1 signature, to # prevent tampering and forged envelope addresses. You can have more than @@ -165,6 +176,16 @@ # #milter-rewrite-local = off +# Recipient limit per milter transaction +# PostSRSd rejects emails which exceed this recipient limit, to prevent +# broken or malicious MTAs from consuming arbitrary amounts of memory. The +# default limit matches the Postfix default limit for SMTP transactions. +# +# Default: +# milter-recipient-limit = 1000 +# +#milter-recipient-limit = 1000 + # SRS tag separator # This is the character following the initial SRS0 or SRS1 tag of a generated # sender address. Valid separators are "=", "+", and "-". Unless you have a diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/CMakeLists.txt new/postsrsd-2.4.0/fuzz/CMakeLists.txt --- old/postsrsd-2.3.0/fuzz/CMakeLists.txt 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/CMakeLists.txt 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1,93 @@ +# PostSRSd - Sender Rewriting Scheme daemon for Postfix +# Copyright 2012-2026 Timo Röhling <[email protected]> +# SPDX-License-Identifier: GPL-3.0-only +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +find_program(AFL_FUZZ afl-fuzz) + +add_library( + postsrsd_fuzz + fuzz.c + ${PROJECT_SOURCE_DIR}/src/config.c + ${PROJECT_SOURCE_DIR}/src/database.c + ${PROJECT_SOURCE_DIR}/src/endpoint.c + ${PROJECT_SOURCE_DIR}/src/main.c + ${PROJECT_SOURCE_DIR}/src/milter.c + ${PROJECT_SOURCE_DIR}/src/netstring.c + ${PROJECT_SOURCE_DIR}/src/sha1.c + ${PROJECT_SOURCE_DIR}/src/srs.c + ${PROJECT_SOURCE_DIR}/src/srs2.c + ${PROJECT_SOURCE_DIR}/src/util.c +) +target_compile_definitions( + postsrsd_fuzz PRIVATE POSTSRSD_FUZZING _GNU_SOURCE _FILE_OFFSET_BITS=64 +) +target_include_directories( + postsrsd_fuzz PUBLIC ${PROJECT_SOURCE_DIR}/src ${PROJECT_BINARY_DIR} +) +target_compile_options( + postsrsd_fuzz PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer +) +target_link_options(postsrsd_fuzz PRIVATE -fsanitize=address,undefined) +target_compile_features(postsrsd_fuzz PRIVATE c_std_99) +target_link_libraries( + postsrsd_fuzz + PRIVATE libconfuse::confuse + $<$<BOOL:${WITH_SQLITE}>:sqlite3::sqlite3> + $<$<BOOL:${WITH_REDIS}>:${HIREDIS_TARGET}> + $<$<BOOL:${WITH_SECCOMP}>:seccomp::seccomp> + ${LIBSOCKET} + ${LIBNSL} + ${LIBNETWORK} +) + +function(add_fuzzer_harness name) + cmake_parse_arguments(arg "" "SEED;DICT" "" ${ARGN}) + add_executable(${name}_harness ${name}.c) + target_compile_options( + ${name}_harness PRIVATE -fsanitize=fuzzer,address,undefined + -fno-omit-frame-pointer + ) + target_link_options( + ${name}_harness PRIVATE -fsanitize=fuzzer,address,undefined + ) + target_link_libraries(${name}_harness PRIVATE postsrsd_fuzz) + file(MAKE_DIRECTORY ${PROJECT_BINARY_DIR}/FUZZING/${name}) + set(fuzzer_call ${AFL_FUZZ} -G 4096 -o + "${PROJECT_BINARY_DIR}/FUZZING/${name}" + ) + if(arg_SEED) + list(APPEND fuzzer_call -i + "${CMAKE_CURRENT_SOURCE_DIR}/seeds/${arg_SEED}" + ) + endif() + if(arg_DICT) + list(APPEND fuzzer_call -x + "${CMAKE_CURRENT_SOURCE_DIR}/dict/${arg_DICT}" + ) + endif() + list(APPEND fuzzer_call "--" "$<TARGET_FILE:${name}_harness>") + message(STATUS "${fuzzer_call}") + add_custom_target( + ${name} + "${fuzzer_call}" + COMMAND_EXPAND_LISTS + ) +endfunction() + +add_fuzzer_harness(fuzz_netstring SEED netstring) +add_fuzzer_harness(fuzz_srs SEED srs DICT srs) +add_fuzzer_harness(fuzz_socketmap SEED socketmap DICT socketmap) +add_fuzzer_harness(fuzz_milter SEED milter DICT milter) +add_fuzzer_harness(fuzz_milter_parsers SEED milter_parsers) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/FUZZ_README.rst new/postsrsd-2.4.0/fuzz/FUZZ_README.rst --- old/postsrsd-2.3.0/fuzz/FUZZ_README.rst 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/FUZZ_README.rst 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1,21 @@ +.. + PostSRSd - Sender Rewriting Scheme daemon for Postfix + Copyright 2012-2026 Timo Röhling <[email protected]> + SPDX-License-Identifier: GPL-3.0-only + +====================== +PostSRSd Fuzzing Notes +====================== + +This is a simple fuzzing harness for AFL_ which I used to shake out some bugs:: + + cd path/to/source + mkdir _build && cd _build + cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_C_COMPILER=/usr/bin/afl-clang-lto -DBUILD_FUZZING=ON + cmake --build . --target $FUZZING_TARGET + cmake --build . --target + +Currently implemented fuzzing targets are: +``fuzz_netstring``, ``fuzz_srs``, ``fuzz_socketmap``. + +.. _AFL: https://aflplus.plus/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/dict/milter new/postsrsd-2.4.0/fuzz/dict/milter --- old/postsrsd-2.3.0/fuzz/dict/milter 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/dict/milter 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1,13 @@ +"O" +"C" +"H" +"M" +"R" +"L" +"N" +"B" +"E" +"A" +"Q" +"D" +"6" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/dict/socketmap new/postsrsd-2.4.0/fuzz/dict/socketmap --- old/postsrsd-2.3.0/fuzz/dict/socketmap 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/dict/socketmap 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1,4 @@ +"forward " +"reverse " +"@" + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/dict/srs new/postsrsd-2.4.0/fuzz/dict/srs --- old/postsrsd-2.3.0/fuzz/dict/srs 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/dict/srs 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1,7 @@ +"SRS0" +"SRS1" +"=" +"+" +"-" +"@" + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/fuzz.c new/postsrsd-2.4.0/fuzz/fuzz.c --- old/postsrsd-2.3.0/fuzz/fuzz.c 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/fuzz.c 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1,104 @@ +/* PostSRSd - Sender Rewriting Scheme daemon for Postfix + * Copyright 2012-2026 Timo Röhling <[email protected]> + * SPDX-License-Identifier: GPL-3.0-only + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "fuzz.h" + +#include <stdlib.h> +#include <sys/socket.h> +#include <sys/wait.h> + +postsrsd_t* fuzz_state_create() +{ + postsrsd_t* state = malloc(sizeof(postsrsd_t)); + if (state == NULL) + return NULL; + init_state(state); + + state->cfg = config_defaults(); + if (state->cfg == NULL) + goto fail; + cfg_setstr(state->cfg, "chroot-dir", ""); + cfg_setstr(state->cfg, "unprivileged-user", ""); + cfg_setbool(state->cfg, "seccomp", cfg_false); + cfg_setint(state->cfg, "keep-alive", 1); + cfg_setint(state->cfg, "milter-recipient-limit", 1000); + cfg_setbool(state->cfg, "always-rewrite", cfg_true); + + state->srs = srs_new(); + if (state->srs == NULL) + goto fail; + srs_add_secret(state->srs, "tops3cr3t"); + + state->srs_domain = strdup("example.com"); + if (state->srs_domain == NULL) + goto fail; + + state->local_domains = domain_set_create(); + if (state->local_domains == NULL) + goto fail; + domain_set_add(state->local_domains, "example.com"); + return state; + +fail: + finalize_state(state); + free(state); + return NULL; +} + +void fuzz_state_destroy(postsrsd_t* state) +{ + finalize_state(state); + free(state); +} + +void fuzz_client(network_client_t client, const uint8_t* data, size_t size) +{ + int fds[2]; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) < 0) + abort(); + pid_t pid = fork(); + if (pid < 0) + abort(); + if (pid == 0) + { + close(fds[0]); + postsrsd_t* state = fuzz_state_create(); + if (state == NULL) + _exit(1); + handle_socketmap_client(state, fds[1]); + fuzz_state_destroy(state); + close(fds[1]); + _exit(0); + } + close(fds[1]); + if (size > 4096) + size = 4096; + if (write(fds[0], data, size) != (ssize_t)size) + abort(); + shutdown(fds[0], SHUT_WR); + char buffer[4096]; + while (read(fds[0], buffer, sizeof(buffer)) > 0) /* do nothing */ + ; + close(fds[0]); + int status; + if (waitpid(pid, &status, 0) < 0) + abort(); + if (WIFSIGNALED(status)) + abort(); + if (WIFEXITED(status) && WEXITSTATUS(status) != 0) + abort(); +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/fuzz.h new/postsrsd-2.4.0/fuzz/fuzz.h --- old/postsrsd-2.3.0/fuzz/fuzz.h 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/fuzz.h 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1,29 @@ +/* PostSRSd - Sender Rewriting Scheme daemon for Postfix + * Copyright 2012-2026 Timo Röhling <[email protected]> + * SPDX-License-Identifier: GPL-3.0-only + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +#ifndef FUZZ_H +#define FUZZ_H + +#include <main.h> +#include <stdint.h> + +typedef void (*network_client_t)(postsrsd_t*, int); + +postsrsd_t* fuzz_state_create(); +void fuzz_state_destroy(postsrsd_t* state); +void fuzz_client(network_client_t client, const uint8_t* data, size_t size); + +#endif diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/fuzz_milter.c new/postsrsd-2.4.0/fuzz/fuzz_milter.c --- old/postsrsd-2.3.0/fuzz/fuzz_milter.c 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/fuzz_milter.c 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1,10 @@ +#include "fuzz.h" + +#include <stddef.h> +#include <stdint.h> + +int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + fuzz_client(handle_milter_client, data, size); + return 0; +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/fuzz_milter_parsers.c new/postsrsd-2.4.0/fuzz/fuzz_milter_parsers.c --- old/postsrsd-2.3.0/fuzz/fuzz_milter_parsers.c 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/fuzz_milter_parsers.c 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1,17 @@ +#include <milter.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + char* output; + output = milter_parse_macros("i", data, size); + if (output != NULL) + free(output); + list_t* L = list_create(); + milter_parse_str_list(L, data, size); + list_destroy(L, free); + return 0; +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/fuzz_netstring.c new/postsrsd-2.4.0/fuzz/fuzz_netstring.c --- old/postsrsd-2.3.0/fuzz/fuzz_netstring.c 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/fuzz_netstring.c 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1,24 @@ +#include <netstring.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + char output[1024]; + size_t decoded_length = 0; + netstring_decode(data, size, output, sizeof(output), &decoded_length); + + int fds[2]; + if (pipe(fds) < 0) + abort(); + if (size > 4096) + size = 4096; + if (write(fds[1], data, size) != (ssize_t)size) + abort(); + close(fds[1]); + netstring_read(fds[0], output, sizeof(output), &decoded_length); + close(fds[0]); + return 0; +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/fuzz_socketmap.c new/postsrsd-2.4.0/fuzz/fuzz_socketmap.c --- old/postsrsd-2.3.0/fuzz/fuzz_socketmap.c 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/fuzz_socketmap.c 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1,10 @@ +#include "fuzz.h" + +#include <stddef.h> +#include <stdint.h> + +int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + fuzz_client(handle_socketmap_client, data, size); + return 0; +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/fuzz_srs.c new/postsrsd-2.4.0/fuzz/fuzz_srs.c --- old/postsrsd-2.3.0/fuzz/fuzz_srs.c 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/fuzz_srs.c 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1,29 @@ +#include <srs2.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + srs_t* srs = srs_new(); + if (srs == NULL) + abort(); + srs_add_secret(srs, "tops3cr3t"); + srs_set_hashlength(srs, 4); + srs_set_hashmin(srs, 4); + srs->faketime = 1577836860; + + char* input = malloc(size + 1); + if (input == NULL) + abort(); + memcpy(input, data, size); + input[size] = 0; + + char output[4096]; + srs_reverse(srs, output, sizeof(output), input); + + free(input); + srs_free(srs); + return 0; +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/gen_milter.py new/postsrsd-2.4.0/fuzz/seeds/gen_milter.py --- old/postsrsd-2.3.0/fuzz/seeds/gen_milter.py 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/seeds/gen_milter.py 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1,22 @@ +#!/usr/bin/python3 + +from pathlib import Path +import struct + +DIR = Path(__file__).parent / "milter" + + +def pkt(payload: bytes): + return struct.pack(">L", len(payload)) + payload + + +optneg = struct.pack(">cLLL", b"O", 6, 0xFF, 0xFF) +mailfrom = b"M<[email protected]>\x00" +rcptto = b"R<[email protected]>\x00" +eom = b"E" + +(DIR / "valid").write_bytes(pkt(optneg) + pkt(mailfrom) + pkt(rcptto) + pkt(eom)) +(DIR / "optneg").write_bytes(pkt(optneg)) +(DIR / "mailfrom").write_bytes(pkt(mailfrom)) +(DIR / "rcptto").write_bytes(pkt(rcptto)) +(DIR / "eom").write_bytes(pkt(eom)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/gen_milter_parsers.py new/postsrsd-2.4.0/fuzz/seeds/gen_milter_parsers.py --- old/postsrsd-2.3.0/fuzz/seeds/gen_milter_parsers.py 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/seeds/gen_milter_parsers.py 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1,10 @@ +#!/usr/bin/python3 + +from pathlib import Path +import struct + +DIR = Path(__file__).parent / "milter_parsers" + +example_list = [b"i", b"foo", b"bar", b"foobar", b"[email protected]"] + +(DIR / "list").write_bytes(b"\x00".join(example_list)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/gen_socketmap.py new/postsrsd-2.4.0/fuzz/seeds/gen_socketmap.py --- old/postsrsd-2.3.0/fuzz/seeds/gen_socketmap.py 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/seeds/gen_socketmap.py 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1,15 @@ +#!/usr/bin/python3 + +from pathlib import Path + +SEEDS = { + "forward": "forward [email protected]", + "reverse": "reverse [email protected]", +} + +DIR = Path(__file__).parent / "socketmap" + +DIR.mkdir(exist_ok=True) +for seed_file, value in SEEDS.items(): + with open(DIR / seed_file, "w") as f: + f.write(f"{len(value)}:{value},") Binary files old/postsrsd-2.3.0/fuzz/seeds/milter/eom and new/postsrsd-2.4.0/fuzz/seeds/milter/eom differ Binary files old/postsrsd-2.3.0/fuzz/seeds/milter/mailfrom and new/postsrsd-2.4.0/fuzz/seeds/milter/mailfrom differ Binary files old/postsrsd-2.3.0/fuzz/seeds/milter/optneg and new/postsrsd-2.4.0/fuzz/seeds/milter/optneg differ Binary files old/postsrsd-2.3.0/fuzz/seeds/milter/rcptto and new/postsrsd-2.4.0/fuzz/seeds/milter/rcptto differ Binary files old/postsrsd-2.3.0/fuzz/seeds/milter/valid and new/postsrsd-2.4.0/fuzz/seeds/milter/valid differ Binary files old/postsrsd-2.3.0/fuzz/seeds/milter_parsers/list and new/postsrsd-2.4.0/fuzz/seeds/milter_parsers/list differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/netstring/a new/postsrsd-2.4.0/fuzz/seeds/netstring/a --- old/postsrsd-2.3.0/fuzz/seeds/netstring/a 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/seeds/netstring/a 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1,2 @@ +1:a, + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/netstring/empty new/postsrsd-2.4.0/fuzz/seeds/netstring/empty --- old/postsrsd-2.3.0/fuzz/seeds/netstring/empty 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/seeds/netstring/empty 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1 @@ +0:, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/netstring/forward_foobar new/postsrsd-2.4.0/fuzz/seeds/netstring/forward_foobar --- old/postsrsd-2.3.0/fuzz/seeds/netstring/forward_foobar 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/seeds/netstring/forward_foobar 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1 @@ +17:forward foo@bar, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/netstring/test new/postsrsd-2.4.0/fuzz/seeds/netstring/test --- old/postsrsd-2.3.0/fuzz/seeds/netstring/test 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/seeds/netstring/test 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1,2 @@ +4:test, + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/socketmap/forward new/postsrsd-2.4.0/fuzz/seeds/socketmap/forward --- old/postsrsd-2.3.0/fuzz/seeds/socketmap/forward 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/seeds/socketmap/forward 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1 @@ +28:forward [email protected], \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/socketmap/reverse new/postsrsd-2.4.0/fuzz/seeds/socketmap/reverse --- old/postsrsd-2.3.0/fuzz/seeds/socketmap/reverse 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/seeds/socketmap/reverse 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1 @@ +53:reverse [email protected], \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/srs/malformed1 new/postsrsd-2.4.0/fuzz/seeds/srs/malformed1 --- old/postsrsd-2.3.0/fuzz/seeds/srs/malformed1 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/seeds/srs/malformed1 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1 @@ +SRS0= \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/srs/malformed2 new/postsrsd-2.4.0/fuzz/seeds/srs/malformed2 --- old/postsrsd-2.3.0/fuzz/seeds/srs/malformed2 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/seeds/srs/malformed2 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1 @@ +SRS0=AAAA= \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/srs/malformed3 new/postsrsd-2.4.0/fuzz/seeds/srs/malformed3 --- old/postsrsd-2.3.0/fuzz/seeds/srs/malformed3 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/seeds/srs/malformed3 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1 @@ +SRS0=AAAA=AA= \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/srs/plain new/postsrsd-2.4.0/fuzz/seeds/srs/plain --- old/postsrsd-2.3.0/fuzz/seeds/srs/plain 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/seeds/srs/plain 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1 @@ [email protected] \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/srs/valid new/postsrsd-2.4.0/fuzz/seeds/srs/valid --- old/postsrsd-2.3.0/fuzz/seeds/srs/valid 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/fuzz/seeds/srs/valid 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1 @@ [email protected] \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/src/config.c new/postsrsd-2.4.0/src/config.c --- old/postsrsd-2.3.0/src/config.c 2026-07-17 19:04:57.000000000 +0200 +++ new/postsrsd-2.4.0/src/config.c 2026-08-12 10:28:49.000000000 +0200 @@ -68,6 +68,18 @@ return 0; } +static int validate_hash_size(cfg_t* cfg, cfg_opt_t* opt) +{ + int value = cfg_opt_getnint(opt, cfg_opt_size(opt) - 1); + if (value < 1 || value > 20) + { + cfg_error(cfg, "option '%s' must be between 1 and 20 inclusive.", + cfg_opt_name(opt)); + return -1; + } + return 0; +} + static bool is_valid_domain_name(const char* s) { char prev = 0; @@ -79,7 +91,7 @@ { if (*s == '.' && prev == '.') return false; - if (!isalnum(*s) && *s != '-' && *s != '.') + if (!isalnum((unsigned char)*s) && *s != '-' && *s != '.') return false; prev = *s++; } @@ -170,8 +182,10 @@ CFG_BOOL("always-rewrite", cfg_false, CFGF_NONE), CFG_STR("socketmap", "unix:/var/spool/postfix/srs", CFGF_NONE), CFG_INT("keep-alive", 30, CFGF_NONE), + CFG_INT("connection-limit", 200, CFGF_NONE), CFG_STR("milter", NULL, CFGF_NODEFAULT), CFG_BOOL("milter-rewrite-local", cfg_false, CFGF_NONE), + CFG_INT("milter-recipient-limit", 1000, CFGF_NONE), CFG_STR("secrets-file", DEFAULT_SECRETS_FILE, CFGF_NONE), CFG_STR("envelope-database", NULL, CFGF_NODEFAULT), CFG_STR("pid-file", NULL, CFGF_NODEFAULT), @@ -187,7 +201,11 @@ cfg_set_validate_func(cfg, "separator", validate_separator); cfg_set_validate_func(cfg, "srs-domain", validate_domain_names); cfg_set_validate_func(cfg, "domains", validate_domain_names); + cfg_set_validate_func(cfg, "hash-length", validate_hash_size); + cfg_set_validate_func(cfg, "hash-minimum", validate_hash_size); cfg_set_validate_func(cfg, "keep-alive", validate_uint); + cfg_set_validate_func(cfg, "connection-limit", validate_uint); + cfg_set_validate_func(cfg, "milter-recipient-limit", validate_uint); cfg_set_validate_func(cfg, "unprivileged-user", validate_unprivileged_user); return cfg; } @@ -280,10 +298,18 @@ srs_t* srs_from_config(cfg_t* cfg) { + int hash_length = cfg_getint(cfg, "hash-length"); + int hash_minimum = cfg_getint(cfg, "hash-minimum"); + if (hash_minimum > hash_length) + { + log_error("hash-minimum (%d) exceeds hash-length (%d)", hash_minimum, + hash_length); + return NULL; + } srs_t* srs = srs_new(); srs_set_alwaysrewrite(srs, cfg_getbool(cfg, "always-rewrite")); - srs_set_hashlength(srs, cfg_getint(cfg, "hash-length")); - srs_set_hashmin(srs, cfg_getint(cfg, "hash-minimum")); + srs_set_hashlength(srs, hash_length); + srs_set_hashmin(srs, hash_minimum); srs_set_separator(srs, cfg_getstr(cfg, "separator")[0]); char* secrets_file = cfg_getstr(cfg, "secrets-file"); if (NONEMPTY_STRING(secrets_file)) @@ -375,9 +401,9 @@ *end = 0; else end = domain + strlen(domain); - while (isspace(domain[0])) + while (isspace((unsigned char)domain[0])) ++domain; - while (end != domain && isspace(*(end - 1))) + while (end != domain && isspace((unsigned char)*(end - 1))) *--end = 0; if (NULL_OR_EMPTY_STRING(domain)) continue; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/src/database.c new/postsrsd-2.4.0/src/database.c --- old/postsrsd-2.3.0/src/database.c 2026-07-17 19:04:57.000000000 +0200 +++ new/postsrsd-2.4.0/src/database.c 2026-08-12 10:28:49.000000000 +0200 @@ -70,7 +70,9 @@ } if (result == SQLITE_ROW) { - value = strdup((const char*)sqlite3_column_text(db->read_stmt, 0)); + const char* column_text = + (const char*)sqlite3_column_text(db->read_stmt, 0); + value = NONEMPTY_STRING(column_text) ? strdup(column_text) : NULL; } sqlite3_reset(db->read_stmt); sqlite3_clear_bindings(db->read_stmt); @@ -195,7 +197,7 @@ } if (reply->type == REDIS_REPLY_STRING) { - value = strdup(reply->str); + value = NONEMPTY_STRING(reply->str) ? strdup(reply->str) : NULL; } freeReplyObject(reply); return value; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/src/endpoint.c new/postsrsd-2.4.0/src/endpoint.c --- old/postsrsd-2.3.0/src/endpoint.c 2026-07-17 19:04:57.000000000 +0200 +++ new/postsrsd-2.4.0/src/endpoint.c 2026-08-12 10:28:49.000000000 +0200 @@ -97,6 +97,7 @@ endpoint->fd[endpoint->num_fds++] = sock; if (endpoint->lock >= 0) endpoint->path = strdup(path); + umask(old_mask); return true; fail: umask(old_mask); @@ -177,7 +178,7 @@ if (err != 0) { log_error("%s", gai_strerror(err)); - return -1; + return false; } int sock = -1, count = 0, free_fds = POSTSRSD_MAX_FDS - endpoint->num_fds; for (struct addrinfo* it = ai; it; it = it->ai_next) @@ -212,8 +213,8 @@ } freeaddrinfo(ai); if (count == 0 && err != 0) - return -1; - return count; + return false; + return true; } endpoint_t* endpoint_create(const char* s) @@ -225,7 +226,7 @@ return NULL; } result->num_fds = 0; - for (unsigned i = 0; i < result->num_fds; ++i) + for (unsigned i = 0; i < POSTSRSD_MAX_FDS; ++i) result->fd[i] = -1; result->lock = -1; result->path = NULL; @@ -243,6 +244,7 @@ if (create_unix_socket(path, result)) return result; log_error("failed to create endpoint '%s'", s); + endpoint_destroy(result); return NULL; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/src/main.c new/postsrsd-2.4.0/src/main.c --- old/postsrsd-2.3.0/src/main.c 2026-07-17 19:04:57.000000000 +0200 +++ new/postsrsd-2.4.0/src/main.c 2026-08-12 10:28:49.000000000 +0200 @@ -14,14 +14,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#include "config.h" +#include "main.h" + #include "database.h" -#include "endpoint.h" #include "milter.h" #include "netstring.h" #include "postsrsd_build_config.h" #include "srs.h" -#include "util.h" #include <errno.h> #include <fcntl.h> @@ -49,20 +48,7 @@ static bool sd_notify_support = false; static sandbox_t* sandbox = NULL; -struct postsrsd -{ - cfg_t* cfg; - srs_t* srs; - endpoint_t* socketmap; - endpoint_t* milter; - char* srs_domain; - domain_set_t* local_domains; - file_watch_t* file_watch; - int target_uid, target_gid; -}; -typedef struct postsrsd postsrsd_t; - -static void init_state(postsrsd_t* state) +void init_state(postsrsd_t* state) { state->cfg = NULL; state->srs = NULL; @@ -73,9 +59,10 @@ state->file_watch = NULL; state->target_uid = 0; state->target_gid = 0; + state->connection_limit = 0; } -static void finalize_state(postsrsd_t* state) +void finalize_state(postsrsd_t* state) { if (state->file_watch != NULL) { @@ -270,7 +257,7 @@ return true; } -static void handle_socketmap_client(postsrsd_t* state, int conn) +void handle_socketmap_client(postsrsd_t* state, int conn) { database_t* db; if (!prepare_client(state, conn, &db)) @@ -358,7 +345,7 @@ database_disconnect(db); } -static void handle_milter_client(postsrsd_t* state, int conn) +void handle_milter_client(postsrsd_t* state, int conn) { #define MILTER_AWAIT_OPTNEG 0 #define MILTER_AWAIT_MAIL 1 @@ -373,6 +360,8 @@ return; const bool always_rewrite = cfg_getbool(state->cfg, "always-rewrite"); const bool rewrite_local = cfg_getbool(state->cfg, "milter-rewrite-local"); + const size_t milter_recipient_limit = + cfg_getint(state->cfg, "milter-recipient-limit"); const int keep_alive = cfg_getint(state->cfg, "keep-alive"); int milter_state = MILTER_AWAIT_OPTNEG; char* queue_id = NULL; @@ -474,6 +463,16 @@ goto done; goto cleanup; } + if (list_size(recipients) >= milter_recipient_limit) + { + log_error( + "%s: MTA exceeded the recipient limit per mail " + "transaction", + queue_id); + if (!milter_reject(conn)) + goto done; + goto cleanup; + } list_append(recipients, strndup(buffer + 1, len - 1)); if (!milter_continue(conn)) goto done; @@ -727,6 +726,7 @@ } /* If we reached this point, the new configuration is valid, so we commit */ finalize_state(state); + new_state.connection_limit = cfg_getint(new_state.cfg, "connection-limit"); *state = new_state; return true; fail: @@ -817,6 +817,7 @@ } while (pid > 0); } +#ifndef POSTSRSD_FUZZING int main(int argc, char** argv) { postsrsd_t state; @@ -824,12 +825,12 @@ FILE* pf = NULL; pid_set_t* P = NULL; int exit_code = EXIT_FAILURE; -#ifdef HAVE_CLOSE_RANGE +# ifdef HAVE_CLOSE_RANGE close_range(3, ~0U, 0); -#else +# else for (int fd = 3; fd < 1024; ++fd) close(fd); -#endif +# endif if (!setup_state(argc, argv, &state)) goto shutdown; sandbox = sandbox_init(); @@ -942,6 +943,12 @@ log_perror(errno, "accept"); continue; } + if (pid_set_size(P) >= state.connection_limit) + { + log_warn("connection limit reached"); + close(conn); + continue; + } pid_t pid = fork(); if (pid == 0) { @@ -991,3 +998,4 @@ pid_set_destroy(P); return exit_code; } +#endif diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/src/main.h new/postsrsd-2.4.0/src/main.h --- old/postsrsd-2.3.0/src/main.h 1970-01-01 01:00:00.000000000 +0100 +++ new/postsrsd-2.4.0/src/main.h 2026-08-12 10:28:49.000000000 +0200 @@ -0,0 +1,45 @@ +/* PostSRSd - Sender Rewriting Scheme daemon for Postfix + * Copyright 2012-2026 Timo Röhling <[email protected]> + * SPDX-License-Identifier: GPL-3.0-only + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +#ifndef MAIN_H +#define MAIN_H + +#include "config.h" +#include "endpoint.h" +#include "srs2.h" +#include "util.h" + +struct postsrsd +{ + cfg_t* cfg; + srs_t* srs; + endpoint_t* socketmap; + endpoint_t* milter; + char* srs_domain; + domain_set_t* local_domains; + file_watch_t* file_watch; + int target_uid, target_gid; + size_t connection_limit; +}; +typedef struct postsrsd postsrsd_t; + +void init_state(postsrsd_t* state); +void finalize_state(postsrsd_t* state); + +void handle_socketmap_client(postsrsd_t* state, int conn); +void handle_milter_client(postsrsd_t* state, int conn); + +#endif diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/src/milter.c new/postsrsd-2.4.0/src/milter.c --- old/postsrsd-2.3.0/src/milter.c 2026-07-17 19:04:57.000000000 +0200 +++ new/postsrsd-2.4.0/src/milter.c 2026-08-12 10:28:49.000000000 +0200 @@ -62,14 +62,11 @@ len -= read_len; while (len > 0) { - read_len = len < sizeof(discardpile) ? len : sizeof(discardpile); - r = read(fd, discardpile, read_len); + size_t discard_len = + len < sizeof(discardpile) ? len : sizeof(discardpile); + r = read(fd, discardpile, discard_len); if (r <= 0) - { - if (r < 0 && errno == EINTR) - continue; break; - } len -= r; if (truncated != NULL) *truncated += r; @@ -137,6 +134,8 @@ size_t numv = list_size(L); if (numv == 0) return milter_send(fd, action); + if (numv > 127) + return false; struct iovec iov[numv + 1]; size_t j = 1, length = 0; for (size_t i = 0; i < numv; ++i) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/src/netstring.c new/postsrsd-2.4.0/src/netstring.c --- old/postsrsd-2.3.0/src/netstring.c 2026-07-17 19:04:57.000000000 +0200 +++ new/postsrsd-2.4.0/src/netstring.c 2026-08-12 10:28:49.000000000 +0200 @@ -28,29 +28,36 @@ if (data == NULL) return NULL; int i = snprintf(buffer, bufsize, "%zu:", length); - if (i <= 0 || length >= bufsize - i) + if (i <= 0 || (size_t)i >= bufsize || length >= bufsize - i) return NULL; - strncpy(&buffer[i], data, length); + memcpy(&buffer[i], data, length); buffer[length + i] = ','; if (encoded_length != NULL) *encoded_length = length + i + 1; return buffer; } -char* netstring_decode(const char* netstring, char* buffer, size_t bufsize, - size_t* decoded_length) +char* netstring_decode(const char* netstring, size_t encoded_length, + char* buffer, size_t bufsize, size_t* decoded_length) { if (netstring == NULL) return NULL; - int i = -1; - size_t length; - if (sscanf(netstring, "%5zu%n", &length, &i) < 1) - return NULL; - if (i < 0 || length >= bufsize) + size_t i = 0; + size_t length = 0; + while (i < encoded_length && netstring[i] != ':') + { + if (netstring[i] < '0' || netstring[i] > '9') + return NULL; + length = 10 * length + (netstring[i] - '0'); + if (length > 100000 || length >= bufsize) + return NULL; + ++i; + } + if (i == 0 || length >= bufsize || length + i + 1 >= encoded_length) return NULL; if (netstring[i] != ':' || netstring[length + i + 1] != ',') return NULL; - strncpy(buffer, &netstring[i + 1], length); + memcpy(buffer, &netstring[i + 1], length); if (decoded_length != NULL) *decoded_length = length; buffer[length] = 0; @@ -110,6 +117,8 @@ if (data == NULL) return -1; size_t n = snprintf(prefix, sizeof(prefix), "%zu:", length); + if (n >= sizeof(prefix)) + return -1; iov[0].iov_base = (void*)prefix; iov[0].iov_len = n; iov[1].iov_base = (void*)data; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/src/netstring.h new/postsrsd-2.4.0/src/netstring.h --- old/postsrsd-2.3.0/src/netstring.h 2026-07-17 19:04:57.000000000 +0200 +++ new/postsrsd-2.4.0/src/netstring.h 2026-08-12 10:28:49.000000000 +0200 @@ -22,8 +22,8 @@ char* netstring_encode(const char* data, size_t length, char* buffer, size_t bufsize, size_t* encoded_length); -char* netstring_decode(const char* netstring, char* buffer, size_t bufsize, - size_t* decoded_length); +char* netstring_decode(const char* netstring, size_t encoded_length, + char* buffer, size_t bufsize, size_t* decoded_length); char* netstring_read(int fd, char* buffer, size_t bufsize, size_t* decoded_length); int netstring_write(int fd, const char* data, size_t length); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/src/srs.c new/postsrsd-2.4.0/src/srs.c --- old/postsrsd-2.3.0/src/srs.c 2026-07-17 19:04:57.000000000 +0200 +++ new/postsrsd-2.4.0/src/srs.c 2026-08-12 10:28:49.000000000 +0200 @@ -149,7 +149,7 @@ { if (db != NULL) { - char* p = buffer; + unsigned char* p = (unsigned char*)buffer; while (*p) { *p = toupper(*p); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/src/srs2.c new/postsrsd-2.4.0/src/srs2.c --- old/postsrsd-2.3.0/src/srs2.c 2026-07-17 19:04:57.000000000 +0200 +++ new/postsrsd-2.4.0/src/srs2.c 2026-08-12 10:28:49.000000000 +0200 @@ -34,6 +34,30 @@ static srs_realloc_t srs_f_realloc = realloc; static srs_free_t srs_f_free = free; +int base64_equivalent(const char* s1, const char* s2, size_t length) +{ + static const int TRANSLATION_START = 43; + static const char TRANSLATION[] = { + '-', 0, '-', 0, '_', '0', '1', '2', '3', '4', '5', '6', '7', '8', + '9', 0, 0, 0, 0, 0, 0, 0, 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', 0, 0, 0, 0, '_', 0, 'a', 'b', + 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', + 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; + int comp = 0; + for (size_t i = 0; i < length; ++i) + { + int h1 = (int)s1[i] - TRANSLATION_START; + int h2 = (int)s2[i] - TRANSLATION_START; + int v1 = + (h1 >= 0 && (size_t)h1 < sizeof(TRANSLATION)) ? TRANSLATION[h1] : 0; + int v2 = + (h2 >= 0 && (size_t)h2 < sizeof(TRANSLATION)) ? TRANSLATION[h2] : 0; + comp |= (v1 ^ v2) | (v1 == 0) | (v2 == 0); + } + return comp == 0; +} + int srs_set_malloc(srs_malloc_t m, srs_realloc_t r, srs_free_t f) { srs_f_malloc = m; @@ -212,7 +236,7 @@ then = 0; for (sp = stamp; *sp; sp++) { - bp = strchr(SRS_TIME_BASECHARS, toupper(*sp)); + bp = strchr(SRS_TIME_BASECHARS, toupper((unsigned char)*sp)); if (bp == NULL) return SRS_EBADTIMESTAMPCHAR; off = bp - SRS_TIME_BASECHARS; @@ -272,8 +296,8 @@ char lcdata[len + 1]; for (j = 0; j < len; j++) { - if (isupper(data[j])) - lcdata[j] = tolower(data[j]); + if (isupper((unsigned char)data[j])) + lcdata[j] = tolower((unsigned char)data[j]); else lcdata[j] = data[j]; } @@ -345,7 +369,7 @@ { va_list ap; int len; - int i, j; + int i; len = strlen(hash); if (len < srs->hashmin) @@ -361,16 +385,7 @@ va_start(ap, nargs); srs_hash_create_v(srs, i, srshash, nargs, ap); va_end(ap); - if (strncasecmp(hash, srshash, len) == 0) - return SRS_SUCCESS; - for (j = 0; j < len; ++j) - { - if (srshash[j] == '-') - srshash[j] = '+'; - if (srshash[j] == '_') - srshash[j] = '/'; - } - if (strncasecmp(hash, srshash, len) == 0) + if (base64_equivalent(hash, srshash, len)) return SRS_SUCCESS; } @@ -619,7 +634,9 @@ int srs_reverse(srs_t* srs, char* buf, unsigned buflen, const char* sender) { char* tmp; - unsigned len; + unsigned len = strlen(sender); + if (len < 5) + return SRS_ENOTSRSADDRESS; if (!SRS_IS_SRS_ADDRESS(sender)) return SRS_ENOTSRSADDRESS; @@ -627,7 +644,6 @@ if (srs->noreverse) return SRS_ENOTREWRITTEN; - len = strlen(sender); if (len >= buflen) return SRS_EBUFTOOSMALL; char senduser[len + 1]; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/src/util.c new/postsrsd-2.4.0/src/util.c --- old/postsrsd-2.3.0/src/util.c 2026-07-17 19:04:57.000000000 +0200 +++ new/postsrsd-2.4.0/src/util.c 2026-08-12 10:28:49.000000000 +0200 @@ -207,11 +207,7 @@ { ssize_t r = read(fd, buffer + total, size - total); if (r <= 0) - { - if (r < 0 && errno == EINTR) - continue; return false; - } total += r; } return true; @@ -223,9 +219,7 @@ do { ssize_t written = writev(fd, iov + i, numv - i); - if (written < 0 && errno != EINTR) - return false; - if (written == 0) + if (written <= 0) return false; while (written > 0) { @@ -369,6 +363,11 @@ return P; } +size_t pid_set_size(pid_set_t* P) +{ + return P != NULL ? P->size : 0; +} + bool pid_set_add(pid_set_t* P, pid_t pid) { if (P == NULL) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/src/util.h new/postsrsd-2.4.0/src/util.h --- old/postsrsd-2.3.0/src/util.h 2026-07-17 19:04:57.000000000 +0200 +++ new/postsrsd-2.4.0/src/util.h 2026-08-12 10:28:49.000000000 +0200 @@ -73,6 +73,7 @@ void domain_set_destroy(domain_set_t* D); pid_set_t* pid_set_create(); +size_t pid_set_size(pid_set_t* P); bool pid_set_add(pid_set_t* P, pid_t pid); bool pid_set_remove(pid_set_t* P, pid_t pid); bool pid_set_kill(pid_set_t* P, int signal); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/tests/blackbox/milter.py new/postsrsd-2.4.0/tests/blackbox/milter.py --- old/postsrsd-2.3.0/tests/blackbox/milter.py 2026-07-17 19:04:57.000000000 +0200 +++ new/postsrsd-2.4.0/tests/blackbox/milter.py 2026-08-12 10:28:49.000000000 +0200 @@ -233,6 +233,16 @@ code = milter_read(sock_stream)[:1] assert code == b"r", "milter should have rejected" + def too_many_recipients(sock_stream: SockStream): + assert mf_optneg(sock_stream), "milter option negotiation failed" + assert mf_envfrom(sock_stream, "a" * 508), "milter MAIL command failed" + code = None + for number in range(10): + code = mf_rcptto(sock_stream, f"recipient{number}@example.com") + if code == b"r": + break + assert code == b"r", "milter should have rejected" + def malformed_rcpt_command(sock_stream: SockStream): assert mf_optneg(sock_stream), "milter option negotiation failed" assert mf_envfrom(sock_stream, "a" * 508), "milter MAIL command failed" @@ -282,6 +292,7 @@ malformed_mail_command, oversized_rcpt_command, malformed_rcpt_command, + too_many_recipients, missing_null_terminators, aborted_transaction, close_on_quit, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/tests/blackbox/testhelper.py new/postsrsd-2.4.0/tests/blackbox/testhelper.py --- old/postsrsd-2.3.0/tests/blackbox/testhelper.py 2026-07-17 19:04:57.000000000 +0200 +++ new/postsrsd-2.4.0/tests/blackbox/testhelper.py 2026-08-12 10:28:49.000000000 +0200 @@ -145,6 +145,7 @@ f'domains-file = "{self._tmpdir_path / "postsrsd.domains"}"\n' f'domains-file-watch = {"on" if use_file_watch else "off"}\n' "keep-alive = 1\n" + "milter-recipient-limit = 5\n" 'chroot-dir = ""\n' 'unprivileged-user = ""\n' f'original-envelope = {"embedded" if database == Database.NONE else "database"}\n' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/postsrsd-2.3.0/tests/unit/test_netstring.c new/postsrsd-2.4.0/tests/unit/test_netstring.c --- old/postsrsd-2.3.0/tests/unit/test_netstring.c 2026-07-17 19:04:57.000000000 +0200 +++ new/postsrsd-2.4.0/tests/unit/test_netstring.c 2026-08-12 10:28:49.000000000 +0200 @@ -58,34 +58,38 @@ char* result; size_t length; - result = netstring_decode("8:PostSRSd,", buffer, sizeof(buffer), &length); + result = + netstring_decode("8:PostSRSd,", 12, buffer, sizeof(buffer), &length); ck_assert_ptr_nonnull(result); ck_assert_uint_eq(length, 8); ck_assert_mem_eq(result, "PostSRSd", length); - result = netstring_decode("16:0123456789abcdef,", buffer, sizeof(buffer), - &length); + result = netstring_decode("16:0123456789abcdef,", 21, buffer, + sizeof(buffer), &length); ck_assert_ptr_nonnull(result); ck_assert_uint_eq(length, 16); ck_assert_mem_eq(result, "0123456789abcdef", length); - result = netstring_decode("0:,", buffer, sizeof(buffer), &length); + result = netstring_decode("0:,", 4, buffer, sizeof(buffer), &length); ck_assert_ptr_nonnull(result); ck_assert_uint_eq(length, 0); - result = netstring_decode(NULL, buffer, sizeof(buffer), &length); + result = netstring_decode(NULL, 0, buffer, sizeof(buffer), &length); ck_assert_ptr_null(result); - result = netstring_decode("1a,", buffer, sizeof(buffer), &length); + result = netstring_decode("1a,", 4, buffer, sizeof(buffer), &length); ck_assert_ptr_null(result); - result = netstring_decode("1:a*", buffer, sizeof(buffer), &length); + result = netstring_decode("1:a*", 5, buffer, sizeof(buffer), &length); ck_assert_ptr_null(result); - result = netstring_decode("0x1:a,", buffer, sizeof(buffer), &length); + result = netstring_decode("0x1:a,", 7, buffer, sizeof(buffer), &length); ck_assert_ptr_null(result); - result = netstring_decode("000001:a,", buffer, sizeof(buffer), &length); + result = netstring_decode("2:,", 3, buffer, sizeof(buffer), &length); + ck_assert_ptr_null(result); + + result = netstring_decode("0:", 2, buffer, sizeof(buffer), &length); ck_assert_ptr_null(result); } END_TEST ++++++ _scmsync.obsinfo ++++++ --- /var/tmp/diff_new_pack.81HPbR/_old 2026-08-14 22:07:24.012920617 +0200 +++ /var/tmp/diff_new_pack.81HPbR/_new 2026-08-14 22:07:24.028921199 +0200 @@ -1,5 +1,5 @@ -mtime: 1784308176 -commit: c5fb9d5d524b925af53e1ad74e7e62a0a7a687d8079a831d45e1cb88d6a93641 +mtime: 1786619326 +commit: 7dcedaecf537a9fa2bdc5d222710a54b8c9ecca744f54afe206dc5e86992ca2d url: https://src.opensuse.org/jengelh/postsrsd revision: master ++++++ build.specials.obscpio ++++++ ++++++ build.specials.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/.gitignore new/.gitignore --- old/.gitignore 1970-01-01 01:00:00.000000000 +0100 +++ new/.gitignore 2026-08-13 13:08:46.000000000 +0200 @@ -0,0 +1 @@ +.osc
