[packages/apache-mod_mpm_itk] Rel 4; harden seccomp uid/gid isolation and memory disclosure, fix startup

arekm arekm at pld-linux.org
Thu Sep 3 11:53:02 CEST 2026


commit 12c7939da51a64098d5ace0eb2fb84c09b09f201
Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
Date:   Sun Aug 9 14:10:35 2026 +0200

    Rel 4; harden seccomp uid/gid isolation and memory disclosure, fix startup

 .gitattributes                     |   4 ++
 apache-mod_mpm_itk-README.security | 130 +++++++++++++++++++++++++++++++++++++
 apache-mod_mpm_itk.conf            |   7 +-
 apache-mod_mpm_itk.spec            |  42 +++++++++++-
 assign-user-id-unresolved.patch    |  25 +++++++
 cap-drop-check.patch               |  27 ++++++++
 fail-closed-partial-drop.patch     |  33 ++++++++++
 filter-supplementary-groups.patch  |  71 ++++++++++++++++++++
 force-non-dumpable.patch           |  23 +++++++
 id-range-parse.patch               | 104 +++++++++++++++++++++++++++++
 seccomp-allow-base-id.patch        |  83 +++++++++++++++++++++++
 seccomp-block-introspection.patch  |  94 +++++++++++++++++++++++++++
 seccomp-block-userns.patch         |  77 ++++++++++++++++++++++
 seccomp-fatal-install.patch        |  26 ++++++++
 seccomp-i386-deny-16bit.patch      |  49 ++++++++++++++
 seccomp-warn-unsupported.patch     |  15 +++++
 seccomp-x32.patch                  |  37 +++++++++++
 startup-cap-narrow-check.patch     |  41 ++++++++++++
 uid-range-precheck.patch           |  59 +++++++++++++++++
 warn-mod-http2.patch               |  49 ++++++++++++++
 warn-suid-dumpable.patch           |  34 ++++++++++
 21 files changed, 1026 insertions(+), 4 deletions(-)
---
diff --git a/apache-mod_mpm_itk.spec b/apache-mod_mpm_itk.spec
index 0ae02b4..dcd551d 100644
--- a/apache-mod_mpm_itk.spec
+++ b/apache-mod_mpm_itk.spec
@@ -5,7 +5,7 @@
 Summary:	mod_mpm_itk - allows you to run each of your vhost under a separate uid and gid
 Name:		apache-mod_mpm_itk
 Version:	%{ver}.%{subver}
-Release:	3
+Release:	4
 License:	Apache v2.0
 Group:		Networking/Daemons/HTTP
 Source0:	http://mpm-itk.sesse.net/mpm-itk-%{ver}-%{subver}.tar.gz
@@ -13,6 +13,24 @@ Source0:	http://mpm-itk.sesse.net/mpm-itk-%{ver}-%{subver}.tar.gz
 Source1:	%{name}.conf
 Source2:	%{name}-php.conf
 Source3:	%{name}.tmpfiles
+Source4:	%{name}-README.security
+Patch0:		id-range-parse.patch
+Patch1:		seccomp-x32.patch
+Patch2:		seccomp-allow-base-id.patch
+Patch3:		cap-drop-check.patch
+Patch4:		seccomp-block-userns.patch
+Patch5:		seccomp-fatal-install.patch
+Patch6:		force-non-dumpable.patch
+Patch7:		seccomp-block-introspection.patch
+Patch8:		seccomp-warn-unsupported.patch
+Patch9:		warn-mod-http2.patch
+Patch10:	filter-supplementary-groups.patch
+Patch11:	startup-cap-narrow-check.patch
+Patch12:	uid-range-precheck.patch
+Patch13:	fail-closed-partial-drop.patch
+Patch14:	seccomp-i386-deny-16bit.patch
+Patch15:	warn-suid-dumpable.patch
+Patch16:	assign-user-id-unresolved.patch
 URL:		http://mpm-itk.sesse.net/
 BuildRequires:	%{apxs}
 BuildRequires:	apache-devel >= 2.4.7
@@ -44,6 +62,25 @@ extra fork per request.
 
 %prep
 %setup -q -n mpm-itk-%{ver}-%{subver}
+%patch -P0 -p1
+%patch -P1 -p1
+%patch -P2 -p1
+%patch -P3 -p1
+%patch -P4 -p1
+%patch -P5 -p1
+%patch -P6 -p1
+%patch -P7 -p1
+%patch -P8 -p1
+%patch -P9 -p1
+%patch -P10 -p1
+%patch -P11 -p1
+%patch -P12 -p1
+%patch -P13 -p1
+%patch -P14 -p1
+%patch -P15 -p1
+%patch -P16 -p1
+
+cp -p %{SOURCE4} README.security
 
 %build
 %configure \
@@ -57,6 +94,7 @@ install -d $RPM_BUILD_ROOT{/var/run/php-ug,%{systemdtmpfilesdir}}
 
 install -p .libs/mpm_itk.so $RPM_BUILD_ROOT%{_pkglibdir}
 cp -p %{SOURCE1} $RPM_BUILD_ROOT%{_sysconfdir}/conf.d/10_mod_%{mod_name}.conf
+sed -i -e 's, at DOCDIR@,%{_docdir}/%{name}-%{version},g' $RPM_BUILD_ROOT%{_sysconfdir}/conf.d/10_mod_%{mod_name}.conf
 cp -p %{SOURCE2} $RPM_BUILD_ROOT%{_sysconfdir}/conf.d/99_mod_%{mod_name}-php.conf
 cp -p %{SOURCE3} $RPM_BUILD_ROOT%{systemdtmpfilesdir}/php-itk-dirs.conf
 
@@ -73,7 +111,7 @@ fi
 
 %files
 %defattr(644,root,root,755)
-%doc CHANGES README
+%doc CHANGES README README.security
 %attr(640,root,root) %config(noreplace) %verify(not md5 mtime size) %{_sysconfdir}/conf.d/*_mod_%{mod_name}.conf
 %attr(640,root,root) %config(noreplace) %verify(not md5 mtime size) %{_sysconfdir}/conf.d/*_mod_%{mod_name}-php.conf
 %attr(755,root,root) %{_pkglibdir}/mpm_itk.so
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..7b4a2d0
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,4 @@
+# Patch files carry unified-diff blank context lines (a blank context line is a
+# single space); that is not editable trailing whitespace, so keep git's
+# whitespace checks off them.
+*.patch -whitespace
diff --git a/apache-mod_mpm_itk-README.security b/apache-mod_mpm_itk-README.security
new file mode 100644
index 0000000..743256c
--- /dev/null
+++ b/apache-mod_mpm_itk-README.security
@@ -0,0 +1,130 @@
+apache-mod_mpm_itk security notes
+=================================
+
+mpm-itk runs each virtual host under a separate uid/gid, so one vhost's files
+need not be readable by the others. That is useful isolation between COOPERATIVE
+sites, but it is NOT a security boundary between MUTUALLY HOSTILE tenants -
+especially when an in-process interpreter such as mod_php is used.
+
+What mpm-itk enforces
+---------------------
+
+* Per-request privilege drop. Each request is served by a child that setgid's,
+  filters its supplementary groups, setuid's to the vhost's user, forces itself
+  non-dumpable and drops all capabilities. The target uid/gid is range-checked in
+  plain C first, so a misconfigured AssignUserID is refused on every architecture,
+  not only where seccomp runs; an AssignUserID that does not yield a usable
+  uid/gid is refused too, rather than falling back to the base Apache user.
+  Every step is checked: a failed or partial drop closes the connection instead
+  of serving.
+
+* LimitUIDRange/LimitGIDRange. A seccomp filter blocks setuid/setgid outside the
+  configured range (uid/gid 0 is always blocked, and a 0 range floor is rejected
+  at config time). The filter also permits exactly the base http uid/gid (51) so
+  Apache can start; a request may run as that id only through a vhost with no
+  AssignUserID - an explicit AssignUserID or an AssignUserIDExpr/AssignGroupIDExpr
+  result naming the base user is range-checked like any other, so request data
+  cannot select it. User-namespace creation is denied so the range cannot be
+  sidestepped. Supplementary groups are filtered to the same gid range, so a
+  tenant never receives a group (including gid 0) outside it. Leave capabilities
+  enabled (the default): EnableCapabilities Off makes mod_unixd keep uid 0, which
+  the filter denies, so the worker would not start.
+  Under an LSM, the filter install itself needs permission: the kernel allows it
+  only with no_new_privs or CAP_SYS_ADMIN, and it tests the capability without
+  auditing it. An AppArmor profile for httpd that does not grant "capability
+  sys_admin" therefore refuses the install silently - no denial is logged - and
+  mpm-itk fails the child rather than serve unrestricted, which stops the server.
+  Grant that capability in the profile covering /usr/sbin/httpd itself; the hats
+  a request runs in do not need it.
+  These seccomp protections (the setuid/setgid range, the user-namespace block,
+  the introspection filter, the x32-vs-x86_64 syscall ABI filter) exist only on
+  Linux x86/i386 builds; elsewhere the package runs best-effort and logs a
+  startup WARNING. The primary uid/gid drop and the supplementary-group filter
+  are plain C, so they still hold there - what a non-x86 build loses is the
+  seccomp backstop against injected or pre-drop code.
+
+* Memory isolation for plain-PHP tenants. The child is forced non-dumpable
+  immediately after the uid change, so the kernel makes /proc/self/mem and friends
+  root-owned and refuses a same-uid ptrace; the seccomp filter also denies
+  ptrace/process_vm_readv/process_vm_writev and re-enabling dumpability. An
+  ordinary PHP script therefore cannot read the worker's memory (mod_ssl keys,
+  other vhosts' secrets, inherited connections).
+
+  Because dumpability is a security property here, mpm-itk turns
+  CoreDumpDirectory off at startup and says so in the log: httpd would otherwise
+  re-enable dumpability right after its own setuid, and abort the child - taking
+  the server with it - when the filter refuses.
+
+  Caveat for the dangerous debug mode fs.suid_dumpable=1: it leaves a process
+  dumpable right after setuid. mpm-itk forces non-dumpable as the very next step,
+  shrinking the exposure to a two-syscall window, but setuid() and prctl() are
+  separate syscalls, so a same-uid local attacker could still race it. Zero-race
+  protection needs fs.suid_dumpable=0 (the default); treat a host that sets it to
+  1 as unsupported for mutually hostile tenants.
+
+* mod_http2 is called out. mpm-itk forks per request from a prefork-derived
+  worker, so a module that starts threads in the child would be unsafe. mod_http2
+  disables itself under prefork - the only MPM mpm-itk runs with - so it never
+  reaches that point; mpm-itk logs a startup WARNING rather than refusing to
+  start, because the module is packaged on many hosts that never activate it.
+
+Still NOT a hard boundary
+-------------------------
+
+* Native code in mod_php. A tenant that runs native code in the interpreter -
+  PHP FFI, or a memory-corruption bug in PHP or an extension - reads the worker's
+  secrets straight from its own address space with a plain memory load; no
+  syscall is involved, so nothing in-process can stop it. Keep FFI disabled and
+  do not run mutually hostile tenants in one interpreter.
+
+* Inherited file descriptors. Forcing the child non-dumpable does not cover its
+  own descriptors (unlike /proc/self/mem, a process may always reach its own
+  /proc/self/fd). Native code in the worker can read from or write to one the
+  worker already opened with its privileged access - logs, the scoreboard shared
+  memory, anything a module left open - because an already-open descriptor is
+  never permission-checked again. Plain PHP cannot get there: php://fd is
+  restricted to the CLI SAPI, and reopening /proc/self/fd/N is checked against
+  the target file like any other path. mpm-itk cannot close these descriptors
+  generically (they are live for the request); this is a further reason to run
+  mutually hostile tenants out of process.
+
+* mod_cache quick handler. It serves cached content before mpm-itk changes uid.
+  Set CacheQuickHandler Off under mpm-itk.
+
+* Shared PHP session directory. The default points every tenant's
+  session.save_path at /var/run/php-ug (sticky, so tenants can't delete each
+  other's files), but it is still shared and world-writable: a tenant can fill it
+  or pre-create a victim's session file (cross-tenant DoS). Give each vhost its
+  own tenant-owned directory instead, e.g.
+    php_admin_value session.save_path /var/lib/php/session/<vhost>
+  created mode 0700 and chowned to that vhost's uid.
+
+Host hardening (outside mpm-itk)
+--------------------------------
+
+The kernel and mount options below close cross-tenant gaps the module cannot
+address itself. None of them break a normal, cooperative setup.
+
+* Mount tenant-writable filesystems (web roots, upload/tmp dirs,
+  session.save_path) nosuid,nodev, so a tenant cannot drop and exec a setuid
+  binary there. Add noexec where the application tolerates it (many PHP apps
+  exec helpers such as ImageMagick or sendmail, so test first). This is the
+  zero-breakage, host-level counterpart to denying new privileges: it neuters a
+  tenant's own suid files without disabling the system's suid helpers, so
+  mail() and the like keep working.
+
+* Mount /proc with hidepid=2,gid=<monitoring-group> so one tenant cannot see
+  another's processes or command lines (which may carry secrets); whitelist
+  monitoring tools through the gid.
+
+* Set kernel.yama.ptrace_scope=2 host-wide. The worker is already non-dumpable
+  and the seccomp filter denies ptrace, so this only backstops the rest of the
+  host, but it is cheap.
+
+* Keep fs.suid_dumpable=0 (the default); see the memory-isolation note above.
+
+* Give each tenant a private /tmp (systemd PrivateTmp=, or pam_namespace)
+  rather than the shared default.
+
+For mutually hostile tenants use per-tenant isolation instead: separate php-fpm
+pools, CGI/suexec with an execve() boundary, or containers.
diff --git a/apache-mod_mpm_itk.conf b/apache-mod_mpm_itk.conf
index 7843341..08db1de 100644
--- a/apache-mod_mpm_itk.conf
+++ b/apache-mod_mpm_itk.conf
@@ -1,3 +1,6 @@
+# mpm-itk separates vhost uids/gids but is NOT an isolation boundary for
+# mutually hostile tenants (esp. with in-process mod_php). See the security
+# notes in @DOCDIR@/README.security.
 LoadModule mpm_itk_module modules/mpm_itk.so
-LimitUIDRange 1000 9999999999999999
-#LimitGIDRange 999 999
+LimitUIDRange 1000 4294967294
+LimitGIDRange 1000 4294967294
diff --git a/assign-user-id-unresolved.patch b/assign-user-id-unresolved.patch
new file mode 100644
index 0000000..f097d4d
--- /dev/null
+++ b/assign-user-id-unresolved.patch
@@ -0,0 +1,25 @@
+--- mpm-itk-2.4.7-04/mpm_itk.c.orig
++++ mpm-itk-2.4.7-04/mpm_itk.c
+@@ -436,6 +436,22 @@
+     wanted_gid = dconf->gid;
+     wanted_username = dconf->username;
+ 
++    /* A numeric AssignUserID of #-1 or #4294967295 parses to the very value that
++     * means "no AssignUserID here", so such a vhost would quietly serve as the
++     * base Apache user instead of a tenant. (The name form cannot get here:
++     * ap_uname2id()/ap_gname2id() exit at startup for a name they cannot
++     * resolve.) username is set iff the directive was given, so refuse the
++     * request rather than fall back. */
++    if (wanted_username != NULL &&
++        (wanted_uid == (uid_t) -1 || wanted_gid == (gid_t) -1)) {
++        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
++                     "AssignUserID \"%s\" does not yield a usable uid/gid "
++                     "(%u/%u); refusing the request rather than falling back to "
++                     "the base Apache user.", wanted_username,
++                     (unsigned) wanted_uid, (unsigned) wanted_gid);
++        return HTTP_INTERNAL_SERVER_ERROR;
++    }
++
+     if (wanted_uid == -1 || wanted_gid == -1) {
+         wanted_uid = ap_unixd_config.user_id;
+         wanted_gid = ap_unixd_config.group_id;
diff --git a/cap-drop-check.patch b/cap-drop-check.patch
new file mode 100644
index 0000000..4bbed22
--- /dev/null
+++ b/cap-drop-check.patch
@@ -0,0 +1,27 @@
+--- mpm-itk-2.4.7-04/mpm_itk.c.orig
++++ mpm-itk-2.4.7-04/mpm_itk.c
+@@ -393,10 +393,20 @@
+                  * for us, but since we were previously not uid 0 (just a normal
+                  * user with CAP_SETUID), we need to do it ourselves.
+                  */
+-                cap_t caps;
+-                caps = cap_init();
+-                cap_clear(caps);
+-                cap_set_proc(caps);
++                cap_t caps = cap_init();
++                if (caps == NULL || cap_clear(caps) != 0 || cap_set_proc(caps) != 0) {
++                    /* We are already the tenant uid but still hold CAP_SETUID etc.
++                     * (setuid() does not clear them here, see above). Serving now
++                     * would run tenant code with those caps, so refuse instead of
++                     * failing open. */
++                    ap_log_error(APLOG_MARK, APLOG_EMERG, errno, NULL,
++                                 "Could not drop capabilities after setuid, closing connection.");
++                    if (caps != NULL) {
++                        cap_free(caps);
++                    }
++                    ap_lingering_close(r->connection);
++                    exit(0);
++                }
+                 cap_free(caps);
+             }
+ #endif
diff --git a/fail-closed-partial-drop.patch b/fail-closed-partial-drop.patch
new file mode 100644
index 0000000..2ed7678
--- /dev/null
+++ b/fail-closed-partial-drop.patch
@@ -0,0 +1,33 @@
+--- mpm-itk-2.4.7-04/mpm_itk.c.orig
++++ mpm-itk-2.4.7-04/mpm_itk.c
+@@ -474,16 +474,20 @@
+                              "This is most likely due to the current LimitGIDRange setting.");
+             }
+             err = 1;
+-        } else if (itk_set_filtered_groups(r, wanted_username, wanted_gid)) {
+-            _DBG("set supplementary groups for %s: %s", wanted_username, strerror(errno));
+-            err = 1;
+-        } else if (setuid(wanted_uid)) {
+-            _DBG("setuid(%d): %s", wanted_uid, strerror(errno));
+-            if (wanted_uid < ap_itk_min_uid || wanted_uid > ap_itk_max_uid) {
+-                ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, NULL,
+-                             "This is most likely due to the current LimitUIDRange setting.");
+-            }
+-            err = 1;
++        } else if (itk_set_filtered_groups(r, wanted_username, wanted_gid) || setuid(wanted_uid)) {
++            /* setgid() has already changed our gid, so we are partially dropped.
++             * Never return into Apache's request handling in that state - close
++             * the connection and exit instead of failing open. A request for
++             * another vhost on a persistent connection lands here as a matter of
++             * course (we no longer hold the capabilities), so only a failure on
++             * the first drop of this child is an emergency. */
++            ap_log_error(APLOG_MARK,
++                         ap_has_irreversibly_setuid ? APLOG_WARNING : APLOG_EMERG,
++                         errno, NULL,
++                         "Could not finish dropping to the tenant credentials "
++                         "after setgid, closing connection.");
++            ap_lingering_close(r->connection);
++            exit(0);
+         } else {
+             /* Force the child non-dumpable immediately after setuid, before the
+              * capability drop. When fs.suid_dumpable=1 the setuid left us dumpable
diff --git a/filter-supplementary-groups.patch b/filter-supplementary-groups.patch
new file mode 100644
index 0000000..83ec65a
--- /dev/null
+++ b/filter-supplementary-groups.patch
@@ -0,0 +1,71 @@
+--- mpm-itk-2.4.7-04/mpm_itk.c.orig
++++ mpm-itk-2.4.7-04/mpm_itk.c
+@@ -316,6 +316,48 @@
+     return OK;
+ }
+ 
++/* Set the tenant's supplementary groups like initgroups() would, but drop any
++ * group outside the configured [min_gid, max_gid] (this also drops gid 0):
++ * LimitGIDRange constrains the primary setgid via seccomp, yet initgroups() would
++ * still hand the tenant every group its user belongs to, unfiltered. Returns 0 on
++ * success, -1 on failure so the caller fails closed. */
++static int itk_set_filtered_groups(request_rec *r, const char *username,
++                                   gid_t primary_gid)
++{
++    /* Guess a size that covers nearly every account: a first call with no buffer
++     * only to learn the count doubles the work, and with a large /etc/group each
++     * call is a full scan of it (an AssignUserID given as #uid matches no user at
++     * all, so the scan always runs to the end). */
++    int ngroups = 32;
++    gid_t *all, *keep;
++    int i, nkeep = 0;
++
++    all = apr_palloc(r->pool, ngroups * sizeof(*all));
++    if (getgrouplist(username, primary_gid, all, &ngroups) < 0) {
++        if (ngroups < 1) {
++            return -1;
++        }
++        all = apr_palloc(r->pool, ngroups * sizeof(*all));
++        if (getgrouplist(username, primary_gid, all, &ngroups) < 0) {
++            return -1;
++        }
++    }
++
++    keep = apr_palloc(r->pool, ngroups * sizeof(*keep));
++    for (i = 0; i < ngroups; i++) {
++        if (all[i] >= ap_itk_min_gid && all[i] <= ap_itk_max_gid) {
++            keep[nkeep++] = all[i];
++        }
++    }
++    if (nkeep < ngroups) {
++        ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server,
++                     "mpm-itk: dropped %d of %d supplementary group(s) for \"%s\" "
++                     "outside the configured gid range", ngroups - nkeep, ngroups,
++                     username);
++    }
++    return (setgroups(nkeep, keep) < 0) ? -1 : 0;
++}
++
+ static int itk_post_perdir_config(request_rec *r)
+ {
+     uid_t wanted_uid;
+@@ -423,8 +465,8 @@
+                              "This is most likely due to the current LimitGIDRange setting.");
+             }
+             err = 1;
+-        } else if (initgroups(wanted_username, wanted_gid)) {
+-            _DBG("initgroups(%s, %d): %s", wanted_username, wanted_gid, strerror(errno));
++        } else if (itk_set_filtered_groups(r, wanted_username, wanted_gid)) {
++            _DBG("set supplementary groups for %s: %s", wanted_username, strerror(errno));
+             err = 1;
+         } else if (setuid(wanted_uid)) {
+             _DBG("setuid(%d): %s", wanted_uid, strerror(errno));
+@@ -788,7 +830,7 @@
+ AP_INIT_TAKE2("LimitGIDRange", limit_gid_range, NULL, RSRC_CONF,
+               "If seccomp v2 is available (Linux 3.5.0+), limit the process's possible "
+               "primary gid to the given range (inclusive endpoints). "
+-              "Note that this does not restrict supplemental gids!"),
++              "Supplemental gids are filtered to the same range."),
+ #if HAVE_LIBCAP
+ AP_INIT_FLAG("EnableCapabilities", enable_caps, NULL, RSRC_CONF,
+              "Drop most root capabilities in the parent process, and instead run as "
diff --git a/force-non-dumpable.patch b/force-non-dumpable.patch
new file mode 100644
index 0000000..dad7587
--- /dev/null
+++ b/force-non-dumpable.patch
@@ -0,0 +1,23 @@
+--- mpm-itk-2.4.7-04/mpm_itk.c.orig
++++ mpm-itk-2.4.7-04/mpm_itk.c
+@@ -387,6 +387,20 @@
+             }
+             err = 1;
+         } else {
++            /* Force the child non-dumpable immediately after setuid, before the
++             * capability drop. When fs.suid_dumpable=1 the setuid left us dumpable
++             * (/proc/PID/mem owned by the tenant uid), so a process sharing that
++             * uid could read this worker's memory - mod_ssl keys, other vhosts'
++             * secrets - in the gap before we lock it down; doing it first closes
++             * that window. The cap drop below only removes capabilities (a subset),
++             * which does not re-set dumpable, so one call is enough. Security
++             * barrier: fail closed. */
++            if (prctl(PR_SET_DUMPABLE, 0)) {
++                ap_log_error(APLOG_MARK, APLOG_EMERG, errno, NULL,
++                             "Could not set the child non-dumpable after setuid, closing connection.");
++                ap_lingering_close(r->connection);
++                exit(0);
++            }
+ #if HAVE_LIBCAP
+             if (ap_itk_enable_caps) {
+                 /* Drop our remaining privileges. Normally setuid() would do this
diff --git a/id-range-parse.patch b/id-range-parse.patch
new file mode 100644
index 0000000..edb79e3
--- /dev/null
+++ b/id-range-parse.patch
@@ -0,0 +1,104 @@
+--- mpm-itk-2.4.7-04/mpm_itk.c.orig
++++ mpm-itk-2.4.7-04/mpm_itk.c
+@@ -518,27 +518,97 @@
+     return NULL;
+ }
+ 
++/* atoi() is undefined for input that does not fit in int, and silently turns a
++ * bound above UINT_MAX into an arbitrary smaller id, narrowing the range the
++ * admin asked for. Such bounds are clamped rather than rejected: no id can
++ * exceed UINT_MAX, so they denote the same range.
++ */
++static const char *parse_id_range_arg(cmd_parms *cmd, const char *directive,
++                                      const char *arg, apr_int64_t *result)
++{
++    char *end;
++    apr_int64_t val = apr_strtoi64(arg, &end, 10);
++
++    if (end == arg || *end != '\0' || val < 0) {
++        return apr_psprintf(cmd->pool,
++                            "%s: \"%s\" is not a valid user or group id",
++                            directive, arg);
++    }
++
++    if (val > UINT_MAX) {
++        ap_log_error(APLOG_MARK, APLOG_WARNING, 0, cmd->server,
++                     "%s: %s is above the largest possible id, using %u instead",
++                     directive, arg, UINT_MAX);
++        val = UINT_MAX;
++    }
++
++    *result = val;
++    return NULL;
++}
++
+ static const char *limit_uid_range(cmd_parms *cmd, void *dummy, const char *min_arg, const char *max_arg)
+ {
++    apr_int64_t min_uid, max_uid;
+     const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY);
+     if (err != NULL) {
+         return err;
+     }
+ 
+-    ap_itk_min_uid = atoi(min_arg);
+-    ap_itk_max_uid = atoi(max_arg);
++    err = parse_id_range_arg(cmd, "LimitUIDRange", min_arg, &min_uid);
++    if (err != NULL) {
++        return err;
++    }
++    err = parse_id_range_arg(cmd, "LimitUIDRange", max_arg, &max_uid);
++    if (err != NULL) {
++        return err;
++    }
++
++    if (min_uid > max_uid) {
++        return apr_psprintf(cmd->pool,
++                            "LimitUIDRange: minimum %s is greater than maximum %s",
++                            min_arg, max_arg);
++    }
++
++    if (min_uid < 1) {
++        return apr_psprintf(cmd->pool,
++                            "LimitUIDRange: minimum must be at least 1; uid 0 must never be permitted");
++    }
++
++    ap_itk_min_uid = (uid_t) min_uid;
++    ap_itk_max_uid = (uid_t) max_uid;
+     return NULL;
+ }
+ 
+ static const char *limit_gid_range(cmd_parms *cmd, void *dummy, const char *min_arg, const char *max_arg)
+ {
++    apr_int64_t min_gid, max_gid;
+     const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY);
+     if (err != NULL) {
+         return err;
+     }
+ 
+-    ap_itk_min_gid = atoi(min_arg);
+-    ap_itk_max_gid = atoi(max_arg);
++    err = parse_id_range_arg(cmd, "LimitGIDRange", min_arg, &min_gid);
++    if (err != NULL) {
++        return err;
++    }
++    err = parse_id_range_arg(cmd, "LimitGIDRange", max_arg, &max_gid);
++    if (err != NULL) {
++        return err;
++    }
++
++    if (min_gid > max_gid) {
++        return apr_psprintf(cmd->pool,
++                            "LimitGIDRange: minimum %s is greater than maximum %s",
++                            min_arg, max_arg);
++    }
++
++    if (min_gid < 1) {
++        return apr_psprintf(cmd->pool,
++                            "LimitGIDRange: minimum must be at least 1; gid 0 must never be permitted");
++    }
++
++    ap_itk_min_gid = (gid_t) min_gid;
++    ap_itk_max_gid = (gid_t) max_gid;
+     return NULL;
+ }
+ 
diff --git a/seccomp-allow-base-id.patch b/seccomp-allow-base-id.patch
new file mode 100644
index 0000000..91ce265
--- /dev/null
+++ b/seccomp-allow-base-id.patch
@@ -0,0 +1,83 @@
+--- mpm-itk-2.4.7-04/mpm_itk.c.orig
++++ mpm-itk-2.4.7-04/mpm_itk.c
+@@ -135,7 +135,8 @@
+          * and get around this. Thus, we disallow setuid(0) if the platform
+          * allows it.
+          */
+-        restrict_setuid_range(ap_itk_min_uid, ap_itk_max_uid, ap_itk_min_gid, ap_itk_max_gid);
++        restrict_setuid_range(ap_itk_min_uid, ap_itk_max_uid, ap_itk_min_gid, ap_itk_max_gid,
++                          ap_unixd_config.user_id, ap_unixd_config.group_id);
+         if (prctl(PR_SET_KEEPCAPS, 1)) {
+             ap_log_error(APLOG_MARK, APLOG_EMERG, errno, NULL, "prctl(PR_SET_KEEPCAPS, 1) failed");
+             exit(APEXIT_CHILDFATAL);
+@@ -143,7 +144,8 @@
+         return OK;
+     }
+ #endif
+-    restrict_setuid_range(ap_itk_min_uid, ap_itk_max_uid, ap_itk_min_gid, ap_itk_max_gid);
++    restrict_setuid_range(ap_itk_min_uid, ap_itk_max_uid, ap_itk_min_gid, ap_itk_max_gid,
++                          ap_unixd_config.user_id, ap_unixd_config.group_id);
+     /* Fiddle with mod_unixd's structures so that it doesn't drop uid 0;
+      * we need that, since we don't have capabilities.
+      */
+--- mpm-itk-2.4.7-04/seccomp.c.orig
++++ mpm-itk-2.4.7-04/seccomp.c
+@@ -128,7 +128,8 @@
+ 
+ #endif
+ 
+-void restrict_setuid_range(uid_t min_uid, uid_t max_uid, gid_t min_gid, gid_t max_gid)
++void restrict_setuid_range(uid_t min_uid, uid_t max_uid, gid_t min_gid, gid_t max_gid,
++                           uid_t base_uid, gid_t base_gid)
+ {
+ #if SECCOMP_BPF_SUPPORTED
+     uid_t min_uid16 = (min_uid > 65535) ? 65535 : min_uid;
+@@ -170,15 +171,19 @@
+         return;
+     }
+ 
++    /* The plain setuid()/setgid() also permit base_uid/base_gid: the User/Group
++     * mod_unixd drops to runs under this same filter, so a range floor above the
++     * base id would otherwise deny that drop and kill the child at startup. The
++     * worker is already this id, so permitting it grants no new privilege. */
+ #ifdef __i386__
+     /* Newer, 32-bit uid_t/gid_t syscalls. */
+     limit_syscall_range(__NR_setfsuid32, 1, min_uid, max_uid, minus_one);
+-    limit_syscall_range(__NR_setuid32, 1, min_uid, max_uid, 0);
++    limit_syscall_range(__NR_setuid32, 1, min_uid, max_uid, base_uid);
+     limit_syscall_range(__NR_setreuid32, 2, min_uid, max_uid, minus_one);
+     limit_syscall_range(__NR_setresuid32, 3, min_uid, max_uid, minus_one);
+ 
+     limit_syscall_range(__NR_setfsgid32, 1, min_gid, max_gid, minus_one);
+-    limit_syscall_range(__NR_setgid32, 1, min_gid, max_gid, 0);
++    limit_syscall_range(__NR_setgid32, 1, min_gid, max_gid, base_gid);
+     limit_syscall_range(__NR_setregid32, 2, min_gid, max_gid, minus_one);
+     limit_syscall_range(__NR_setresgid32, 3, min_gid, max_gid, minus_one);
+ 
+@@ -196,12 +201,12 @@
+ #else // not defined(__i386__)
+     /* Just one set of 32-bit uid_t/gid_t syscalls to worry about. */
+     limit_syscall_range(__NR_setfsuid, 1, min_uid, max_uid, minus_one);
+-    limit_syscall_range(__NR_setuid, 1, min_uid, max_uid, 0);
++    limit_syscall_range(__NR_setuid, 1, min_uid, max_uid, base_uid);
+     limit_syscall_range(__NR_setreuid, 2, min_uid, max_uid, minus_one);
+     limit_syscall_range(__NR_setresuid, 3, min_uid, max_uid, minus_one);
+ 
+     limit_syscall_range(__NR_setfsgid, 1, min_gid, max_gid, minus_one);
+-    limit_syscall_range(__NR_setgid, 1, min_gid, max_gid, 0);
++    limit_syscall_range(__NR_setgid, 1, min_gid, max_gid, base_gid);
+     limit_syscall_range(__NR_setregid, 2, min_gid, max_gid, minus_one);
+     limit_syscall_range(__NR_setresgid, 3, min_gid, max_gid, minus_one);
+ #endif
+--- mpm-itk-2.4.7-04/seccomp.h.orig
++++ mpm-itk-2.4.7-04/seccomp.h
+@@ -29,7 +29,8 @@
+ #ifndef APACHE_MPM_SECCOMP_H
+ #define APACHE_MPM_SECCOMP_H
+ 
+-void restrict_setuid_range(uid_t min_uid, uid_t max_uid, gid_t min_gid, gid_t max_gid);
++void restrict_setuid_range(uid_t min_uid, uid_t max_uid, gid_t min_gid, gid_t max_gid,
++                           uid_t base_uid, gid_t base_gid);
+ 
+ #endif /* AP_MPM_SECCOMP_H */
+ /** @} */
diff --git a/seccomp-block-introspection.patch b/seccomp-block-introspection.patch
new file mode 100644
index 0000000..87586ac
--- /dev/null
+++ b/seccomp-block-introspection.patch
@@ -0,0 +1,94 @@
+--- mpm-itk-2.4.7-04/seccomp.c.orig
++++ mpm-itk-2.4.7-04/seccomp.c
+@@ -171,6 +171,45 @@
+     return apply_seccomp_filter(clone3_filter, pos);
+ }
+ 
++/* Deny a whole syscall with EPERM. Used for the self-memory-introspection
++ * calls: even while the worker is non-dumpable, ptrace()/process_vm_readv()
++ * against its own pid still succeed (ptrace_may_access() short-circuits for the
++ * caller's own thread group), which would expose mod_ssl keys and other vhosts'
++ * secrets to a tenant running native code.
++ */
++static int deny_syscall(int syscall_to_match)
++{
++    static struct sock_filter deny_filter[BPF_MAXINSNS];
++    int pos = 0;
++
++    add_bpf_stmt(deny_filter, &pos, BPF_LD + BPF_W + BPF_ABS, syscall_nr);
++    add_bpf_jump(deny_filter, &pos, BPF_JMP + BPF_JEQ + BPF_K, syscall_to_match, 1, 0);
++    add_bpf_stmt(deny_filter, &pos, BPF_RET + BPF_K, SECCOMP_RET_ALLOW);
++    add_bpf_stmt(deny_filter, &pos, BPF_RET + BPF_K, SECCOMP_RET_ERRNO | EPERM);
++    return apply_seccomp_filter(deny_filter, pos);
++}
++
++/* Deny prctl(PR_SET_DUMPABLE, non-zero). The child is forced non-dumpable right
++ * after setuid; re-enabling dumpability would flip /proc/self/mem back to the
++ * tenant uid and reopen the memory-disclosure path. arg1 == 0 (staying
++ * non-dumpable) stays allowed so our own drop path is unaffected.
++ */
++static int deny_set_dumpable(void)
++{
++    static struct sock_filter dumpable_filter[BPF_MAXINSNS];
++    int pos = 0;
++
++    add_bpf_stmt(dumpable_filter, &pos, BPF_LD + BPF_W + BPF_ABS, syscall_nr);
++    add_bpf_jump(dumpable_filter, &pos, BPF_JMP + BPF_JEQ + BPF_K, __NR_prctl, 0, 5);
++    add_bpf_stmt(dumpable_filter, &pos, BPF_LD + BPF_W + BPF_ABS, syscall_arg(0));
++    add_bpf_jump(dumpable_filter, &pos, BPF_JMP + BPF_JEQ + BPF_K, PR_SET_DUMPABLE, 0, 3);
++    add_bpf_stmt(dumpable_filter, &pos, BPF_LD + BPF_W + BPF_ABS, syscall_arg(1));
++    add_bpf_jump(dumpable_filter, &pos, BPF_JMP + BPF_JEQ + BPF_K, 0, 1, 0);
++    add_bpf_stmt(dumpable_filter, &pos, BPF_RET + BPF_K, SECCOMP_RET_ERRNO | EPERM);
++    add_bpf_stmt(dumpable_filter, &pos, BPF_RET + BPF_K, SECCOMP_RET_ALLOW);
++    return apply_seccomp_filter(dumpable_filter, pos);
++}
++
+ #endif
+ 
+ void restrict_setuid_range(uid_t min_uid, uid_t max_uid, gid_t min_gid, gid_t max_gid,
+@@ -221,6 +260,13 @@
+     deny_clone_newuser(__NR_clone);
+     deny_clone3();
+ 
++    /* Shut the self-memory-introspection paths so a tenant reaching native code
++     * cannot read the worker's inherited secrets out of its own address space. */
++    deny_syscall(__NR_ptrace);
++    deny_syscall(__NR_process_vm_readv);
++    deny_syscall(__NR_process_vm_writev);
++    deny_set_dumpable();
++
+     /* The plain setuid()/setgid() also permit base_uid/base_gid: the User/Group
+      * mod_unixd drops to runs under this same filter, so a range floor above the
+      * base id would otherwise deny that drop and kill the child at startup. The
+--- mpm-itk-2.4.7-04/mpm_itk.c.orig
++++ mpm-itk-2.4.7-04/mpm_itk.c
+@@ -122,6 +122,14 @@
+ 
+ static int itk_pre_drop_privileges(apr_pool_t *pool, server_rec *s)
+ {
++    /* Clear this before mod_unixd reads it: with CoreDumpDirectory configured it
++     * would prctl(PR_SET_DUMPABLE, 1) right after its setuid, the filter below
++     * refuses that, and ap_unixd_setup_child() turns the refusal into a fatal
++     * child error - which prefork escalates into shutting the server down. Doing
++     * it here rather than in post_config also covers mod_cgid's daemon, which
++     * runs these hooks itself and may be forked before our post_config hook. */
++    ap_coredumpdir_configured = 0;
++
+ #if HAVE_LIBCAP
+     if (ap_itk_enable_caps) {
+         /* mod_unixd will drop down to a normal user. This means that even if an
+@@ -259,6 +267,14 @@
+ {
+     int threaded;
+     int ret = ap_mpm_query(AP_MPMQ_IS_THREADED, &threaded) != APR_SUCCESS;
++
++    /* Say once why no core file will ever appear; see itk_pre_drop_privileges(). */
++    if (ap_coredumpdir_configured) {
++        ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s,
++                     "CoreDumpDirectory has no effect under mpm-itk; the children "
++                     "serving requests are deliberately not dumpable.");
++    }
++
+     if (ret != APR_SUCCESS || threaded) {
+         ap_log_perror(APLOG_MARK, APLOG_CRIT, ret, ptemp,
+                       "mpm-itk cannot use threaded MPMs; please use prefork.");
diff --git a/seccomp-block-userns.patch b/seccomp-block-userns.patch
new file mode 100644
index 0000000..f59efa5
--- /dev/null
+++ b/seccomp-block-userns.patch
@@ -0,0 +1,77 @@
+--- mpm-itk-2.4.7-04/seccomp.c.orig
++++ mpm-itk-2.4.7-04/seccomp.c
+@@ -45,6 +45,19 @@
+ #ifndef __X32_SYSCALL_BIT
+ #define __X32_SYSCALL_BIT 0x40000000
+ #endif
++/* For denying user-namespace creation. From <sched.h>/<asm/unistd.h>. */
++#ifndef CLONE_NEWUSER
++#define CLONE_NEWUSER 0x10000000
++#endif
++/* x32 tags its syscall numbers with __X32_SYSCALL_BIT; i386 is __ILP32__ too,
++ * hence the __amd64__ test. */
++#ifndef __NR_clone3
++#if defined(__amd64__) && defined(__ILP32__)
++#define __NR_clone3 (__X32_SYSCALL_BIT + 435)
++#else
++#define __NR_clone3 435
++#endif
++#endif
+ struct seccomp_data {
+     int nr;
+     __u32 arch;
+@@ -126,6 +139,42 @@
+     return apply_seccomp_filter(syscall_filter, pos);
+ }
+ 
++/* Deny <syscall_to_match> when its first argument carries CLONE_NEWUSER, so a
++ * CAP_SETUID worker cannot create a user namespace, map an in-range inner id
++ * onto an out-of-range outer id, and thereby sidestep the setuid range above.
++ */
++static int deny_clone_newuser(int syscall_to_match)
++{
++    static struct sock_filter userns_filter[BPF_MAXINSNS];
++    int pos = 0;
++
++    add_bpf_stmt(userns_filter, &pos, BPF_LD + BPF_W + BPF_ABS, syscall_nr);
++    add_bpf_jump(userns_filter, &pos, BPF_JMP + BPF_JEQ + BPF_K, syscall_to_match, 1, 0);
++    add_bpf_stmt(userns_filter, &pos, BPF_RET + BPF_K, SECCOMP_RET_ALLOW);
++
++    add_bpf_stmt(userns_filter, &pos, BPF_LD + BPF_W + BPF_ABS, syscall_arg(0));
++    add_bpf_jump(userns_filter, &pos, BPF_JMP + BPF_JSET + BPF_K, CLONE_NEWUSER, 0, 1);
++    add_bpf_stmt(userns_filter, &pos, BPF_RET + BPF_K, SECCOMP_RET_ERRNO | EPERM);
++    add_bpf_stmt(userns_filter, &pos, BPF_RET + BPF_K, SECCOMP_RET_ALLOW);
++    return apply_seccomp_filter(userns_filter, pos);
++}
++
++/* clone3() passes its flags in a struct we cannot dereference from seccomp, so
++ * its CLONE_NEWUSER cannot be inspected. Make it fail with ENOSYS; glibc then
++ * falls back to clone(), which deny_clone_newuser() does filter.
++ */
++static int deny_clone3(void)
++{
++    static struct sock_filter clone3_filter[BPF_MAXINSNS];
++    int pos = 0;
++
++    add_bpf_stmt(clone3_filter, &pos, BPF_LD + BPF_W + BPF_ABS, syscall_nr);
++    add_bpf_jump(clone3_filter, &pos, BPF_JMP + BPF_JEQ + BPF_K, __NR_clone3, 1, 0);
++    add_bpf_stmt(clone3_filter, &pos, BPF_RET + BPF_K, SECCOMP_RET_ALLOW);
++    add_bpf_stmt(clone3_filter, &pos, BPF_RET + BPF_K, SECCOMP_RET_ERRNO | ENOSYS);
++    return apply_seccomp_filter(clone3_filter, pos);
++}
++
+ #endif
+ 
+ void restrict_setuid_range(uid_t min_uid, uid_t max_uid, gid_t min_gid, gid_t max_gid,
+@@ -177,6 +226,11 @@
+         return;
+     }
+ 
++    /* Close the user-namespace escape hatch around the range restriction below. */
++    deny_clone_newuser(__NR_unshare);
++    deny_clone_newuser(__NR_clone);
++    deny_clone3();
++
+     /* The plain setuid()/setgid() also permit base_uid/base_gid: the User/Group
+      * mod_unixd drops to runs under this same filter, so a range floor above the
+      * base id would otherwise deny that drop and kill the child at startup. The
diff --git a/seccomp-fatal-install.patch b/seccomp-fatal-install.patch
new file mode 100644
index 0000000..c1b66db
--- /dev/null
+++ b/seccomp-fatal-install.patch
@@ -0,0 +1,26 @@
+--- mpm-itk-2.4.7-04/seccomp.c.orig
++++ mpm-itk-2.4.7-04/seccomp.c
+@@ -83,11 +83,18 @@
+         .filter = filter,
+     };
+     if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &seccomp_prog) != 0) {
+-        ap_log_error(APLOG_MARK, APLOG_INFO, APR_SUCCESS, ap_server_conf,
+-                     "Installing seccomp filter failed (probably due to too old kernel); "
+-                     "unable to restrict setuid privileges. Error was: %s",
+-                     strerror(errno));
+-        return 1;
++        /* Fail closed: LimitUIDRange/GIDRange are a security boundary, so we must
++         * not run unrestricted if the filter cannot be installed. */
++        ap_log_error(APLOG_MARK, APLOG_EMERG, APR_SUCCESS, ap_server_conf,
++                     "Installing seccomp filter failed; refusing to run without the "
++                     "configured setuid/setgid restriction. Error was: %s%s",
++                     strerror(errno),
++                     (errno == EACCES || errno == EPERM) ?
++                     " - the kernel permits the install only with no_new_privs or "
++                     "CAP_SYS_ADMIN, and it tests the capability without auditing it, "
++                     "so an LSM policy that withholds it (an AppArmor profile lacking "
++                     "\"capability sys_admin\") refuses this silently" : "");
++        exit(APEXIT_CHILDFATAL);
+     } else {
+         return 0;
+     }
diff --git a/seccomp-i386-deny-16bit.patch b/seccomp-i386-deny-16bit.patch
new file mode 100644
index 0000000..aa083e5
--- /dev/null
+++ b/seccomp-i386-deny-16bit.patch
@@ -0,0 +1,49 @@
+--- mpm-itk-2.4.7-04/seccomp.c.orig
++++ mpm-itk-2.4.7-04/seccomp.c
+@@ -216,15 +216,7 @@
+                            uid_t base_uid, gid_t base_gid)
+ {
+ #if SECCOMP_BPF_SUPPORTED
+-    uid_t min_uid16 = (min_uid > 65535) ? 65535 : min_uid;
+-    uid_t max_uid16 = (max_uid > 65535) ? 65535 : max_uid;
+-    gid_t min_gid16 = (min_gid > 65535) ? 65535 : min_gid;
+-    gid_t max_gid16 = (max_gid > 65535) ? 65535 : max_gid;
+-
+     uid_t minus_one = (uid_t) -1;
+-#ifdef __i386__
+-    __u16 minus_one16 = (__u16) -1;
+-#endif // defined(__i386__)
+ 
+     /* Apply a seccomp BPF to ourselves that disallows all setuid- and
+      * setgid-like calls if the first argument is 0.  The list of calls comes from
+@@ -283,16 +275,20 @@
+     limit_syscall_range(__NR_setregid32, 2, min_gid, max_gid, minus_one);
+     limit_syscall_range(__NR_setresgid32, 3, min_gid, max_gid, minus_one);
+ 
+-    /* Older 16-bit old_uid_t/old_gid_t syscalls. */
+-    limit_syscall_range(__NR_setfsuid, 1, min_uid16, max_uid16, minus_one16);
+-    limit_syscall_range(__NR_setuid, 1, min_uid16, max_uid16, 0);
+-    limit_syscall_range(__NR_setreuid, 2, min_uid16, max_uid16, minus_one16);
+-    limit_syscall_range(__NR_setresuid, 3, min_uid16, max_uid16, minus_one16);
+-
+-    limit_syscall_range(__NR_setfsgid, 1, min_gid16, max_gid16, minus_one16);
+-    limit_syscall_range(__NR_setgid, 1, min_gid16, max_gid16, 0);
+-    limit_syscall_range(__NR_setregid, 2, min_gid16, max_gid16, minus_one16);
+-    limit_syscall_range(__NR_setresgid, 3, min_gid16, max_gid16, minus_one16);
++    /* Older 16-bit old_uid_t/old_gid_t syscalls. Modern glibc never uses these
++     * (it calls the 32-bit variants above); range-checking them is unsound - the
++     * kernel truncates the id to 16 bits, and clamping the configured range to
++     * 65535 would let a floor above that be sidestepped via setuid(65535). Deny
++     * the whole legacy family instead. */
++    deny_syscall(__NR_setfsuid);
++    deny_syscall(__NR_setuid);
++    deny_syscall(__NR_setreuid);
++    deny_syscall(__NR_setresuid);
++
++    deny_syscall(__NR_setfsgid);
++    deny_syscall(__NR_setgid);
++    deny_syscall(__NR_setregid);
++    deny_syscall(__NR_setresgid);
+ 
+ #else // not defined(__i386__)
+     /* Just one set of 32-bit uid_t/gid_t syscalls to worry about. */
diff --git a/seccomp-warn-unsupported.patch b/seccomp-warn-unsupported.patch
new file mode 100644
index 0000000..089f831
--- /dev/null
+++ b/seccomp-warn-unsupported.patch
@@ -0,0 +1,15 @@
+--- mpm-itk-2.4.7-04/seccomp.c.orig
++++ mpm-itk-2.4.7-04/seccomp.c
+@@ -308,8 +308,10 @@
+ #endif
+ 
+ #else
+-    ap_log_error(APLOG_MARK, APLOG_INFO, APR_SUCCESS, ap_server_conf,
++    /* WARNING, not INFO: at the default LogLevel this would otherwise be
++     * invisible, silently leaving LimitUIDRange/LimitGIDRange unenforced. */
++    ap_log_error(APLOG_MARK, APLOG_WARNING, APR_SUCCESS, ap_server_conf,
+                  "Your platform or architecture does not support seccomp v2; "
+-                 "unable to restrict setuid privileges.");
++                 "LimitUIDRange/LimitGIDRange are NOT enforced here.");
+ #endif
+ }
diff --git a/seccomp-x32.patch b/seccomp-x32.patch
new file mode 100644
index 0000000..de91575
--- /dev/null
+++ b/seccomp-x32.patch
@@ -0,0 +1,37 @@
+--- mpm-itk-2.4.7-04/seccomp.c.orig
++++ mpm-itk-2.4.7-04/seccomp.c
+@@ -40,6 +40,11 @@
+ #define SECCOMP_RET_ERRNO 0x00050000U
+ #define SECCOMP_RET_ALLOW 0x7fff0000U
+ #define SECCOMP_MODE_FILTER 2
++
++/* x32 syscalls share AUDIT_ARCH_X86_64 but carry this bit in the number. */
++#ifndef __X32_SYSCALL_BIT
++#define __X32_SYSCALL_BIT 0x40000000
++#endif
+ struct seccomp_data {
+     int nr;
+     __u32 arch;
+@@ -149,6 +154,22 @@
+         BPF_STMT(BPF_LD + BPF_W + BPF_ABS, arch_nr),
+         BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ARCH_NR, 1, 0),
+         BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL),
++#if defined(__amd64__)
++        /* x32 and x86_64 syscalls both report AUDIT_ARCH_X86_64; only
++         * __X32_SYSCALL_BIT in the number tells them apart, and the __NR_*
++         * constants below carry it on x32 and lack it on x86_64. A syscall
++         * from the other ABI would miss every JEQ below and fall through to
++         * ALLOW, letting setuid(0) slip past the range restriction. Refuse
++         * the ABI this build does not use before the per-syscall checks run.
++         */
++        BPF_STMT(BPF_LD + BPF_W + BPF_ABS, syscall_nr),
++#if defined(__ILP32__)
++        BPF_JUMP(BPF_JMP + BPF_JSET + BPF_K, __X32_SYSCALL_BIT, 1, 0),
++#else
++        BPF_JUMP(BPF_JMP + BPF_JSET + BPF_K, __X32_SYSCALL_BIT, 0, 1),
++#endif
++        BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | EPERM),
++#endif
+         BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
+     };
+     if (apply_seccomp_filter(arch_filter, sizeof(arch_filter) / sizeof(arch_filter[0])) != 0) {
diff --git a/startup-cap-narrow-check.patch b/startup-cap-narrow-check.patch
new file mode 100644
index 0000000..5386fc6
--- /dev/null
+++ b/startup-cap-narrow-check.patch
@@ -0,0 +1,41 @@
+--- mpm-itk-2.4.7-04/mpm_itk.c.orig
++++ mpm-itk-2.4.7-04/mpm_itk.c
+@@ -174,18 +174,28 @@
+             exit(APEXIT_CHILDFATAL);
+         }
+ 
+-        /* Now drop as many privileges as we can. We'll still
+-         * access files with uid=0, and we can setuid() to anything, but
+-         * at least there's tons of other evilness (like loading kernel
+-         * modules) we can't do directly. (The setuid() capability will
+-         * go away automatically when we setuid() or exec() -- the former
+-         * is likely to come first.)
++        /* Narrow to just the capabilities we still need (setuid/setgid per
++         * request, plus DAC_READ_SEARCH/SYS_NICE) and drop everything else. This
++         * runs after mod_unixd's setuid(51) with KEEPCAPS, so the permitted set is
++         * still wide; a failed cap_set_proc() here would leave the worker holding
++         * it (permitted caps can be raised to effective). So every step is checked
++         * and we abort on failure. These caps are NOT cleared by the later
++         * per-request setuid (that only happens transitioning away from uid 0,
++         * which we are not), so the request handler drops them explicitly too.
+          */
+         caps = cap_init();
+-        cap_clear(caps);
+-        cap_set_flag(caps, CAP_PERMITTED, sizeof(suidcaps)/sizeof(cap_value_t), suidcaps, CAP_SET);
+-        cap_set_flag(caps, CAP_EFFECTIVE, sizeof(suidcaps)/sizeof(cap_value_t), suidcaps, CAP_SET);
+-        cap_set_proc(caps);
++        if (caps == NULL ||
++            cap_clear(caps) != 0 ||
++            cap_set_flag(caps, CAP_PERMITTED, sizeof(suidcaps)/sizeof(cap_value_t), suidcaps, CAP_SET) != 0 ||
++            cap_set_flag(caps, CAP_EFFECTIVE, sizeof(suidcaps)/sizeof(cap_value_t), suidcaps, CAP_SET) != 0 ||
++            cap_set_proc(caps) != 0) {
++            ap_log_error(APLOG_MARK, APLOG_EMERG, errno, NULL,
++                         "Could not narrow capabilities at startup, aborting.");
++            if (caps != NULL) {
++                cap_free(caps);
++            }
++            exit(APEXIT_CHILDFATAL);
++        }
+         cap_free(caps);
+         return OK;
+     }
diff --git a/uid-range-precheck.patch b/uid-range-precheck.patch
new file mode 100644
index 0000000..b60452b
--- /dev/null
+++ b/uid-range-precheck.patch
@@ -0,0 +1,59 @@
+--- mpm-itk-2.4.7-04/mpm_itk.c.orig
++++ mpm-itk-2.4.7-04/mpm_itk.c
+@@ -375,6 +375,7 @@
+     uid_t wanted_uid;
+     gid_t wanted_gid;
+     const char *wanted_username;
++    int uid_from_fallback = 0, gid_from_fallback = 0;
+     int err = 0;
+ 
+     itk_server_conf *sconf =
+@@ -422,6 +423,7 @@
+         wanted_uid = ap_unixd_config.user_id;
+         wanted_gid = ap_unixd_config.group_id;
+         wanted_username = ap_unixd_config.user_name;
++        uid_from_fallback = gid_from_fallback = 1;
+     }
+ 
+     /* AssignUserIDExpr and AssignGroupIDExpr override AssignUserID and defaults. */
+@@ -444,6 +446,7 @@
+       }
+ 
+       wanted_uid = ent->pw_uid;
++      uid_from_fallback = 0;
+     }
+     if (dconf->gid_expr != NULL) {
+       struct group *ent;
+@@ -464,6 +467,32 @@
+       }
+ 
+       wanted_gid = ent->gr_gid;
++      gid_from_fallback = 0;
++    }
++
++    /* Refuse an out-of-range primary uid/gid in plain C, before any credential
++     * change: the normal path must not depend on the seccomp filter (absent on
++     * non-x86), and no partial drop must be started for a misconfigured
++     * AssignUserID. The base Apache uid/gid is exempt only when it came from
++     * the no-AssignUserID fallback above: the seccomp base-id exception exists
++     * for mod_unixd's startup drop, and honouring it for an explicit
++     * AssignUserID or an AssignUserIDExpr/AssignGroupIDExpr result would let
++     * request data (an expression built from the Host header, say) select the
++     * base user below the configured floor. So uid/gid 0 is out of range (a 0
++     * floor is rejected at config time) unless Apache itself runs as that id
++     * and the vhost has no AssignUserID. Logged at ERR, not NOTICE: NOTICE is
++     * below the default LogLevel warn, so the 500 would come with no reason in
++     * the log.
++     */
++    if (!(uid_from_fallback ||
++          (wanted_uid >= ap_itk_min_uid && wanted_uid <= ap_itk_max_uid)) ||
++        !(gid_from_fallback ||
++          (wanted_gid >= ap_itk_min_gid && wanted_gid <= ap_itk_max_gid))) {
++        ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server,
++                     "AssignUserID uid/gid %u/%u is outside LimitUIDRange/"
++                     "LimitGIDRange; refusing the request.",
++                     (unsigned) wanted_uid, (unsigned) wanted_gid);
++        return HTTP_INTERNAL_SERVER_ERROR;
+     }
+ 
+     /* setuid() at least on the first request, but from there only if we need to change anything.
diff --git a/warn-mod-http2.patch b/warn-mod-http2.patch
new file mode 100644
index 0000000..08e34f4
--- /dev/null
+++ b/warn-mod-http2.patch
@@ -0,0 +1,49 @@
+--- mpm-itk-2.4.7-04/mpm_itk.c.orig
++++ mpm-itk-2.4.7-04/mpm_itk.c
+@@ -262,6 +262,27 @@
+     }
+ }
+ 
++/* Apache sets module->name from __FILE__: a bare "mod_foo.c" for some builds but
++ * the full build path for others (PLD's bundled httpd modules carry it), and
++ * never the LoadModule symbol. Match on the basename so both forms are caught. */
++static int itk_module_loaded(const char *cfile)
++{
++    int i;
++    for (i = 0; ap_loaded_modules[i] != NULL; i++) {
++        const char *name = ap_loaded_modules[i]->name;
++        const char *base;
++        if (name == NULL) {
++            continue;
++        }
++        base = strrchr(name, '/');
++        base = (base != NULL) ? base + 1 : name;
++        if (strcmp(base, cfile) == 0) {
++            return 1;
++        }
++    }
++    return 0;
++}
++
+ static int itk_init_handler(apr_pool_t *p, apr_pool_t *plog,
+                             apr_pool_t *ptemp, server_rec *s)
+ {
+@@ -281,6 +302,18 @@
+         return HTTP_INTERNAL_SERVER_ERROR;
+     }
+ 
++    /* mpm-itk forks per request from a prefork-derived worker, so a module that
++     * starts threads in the child would break that. mod_http2 is the usual
++     * suspect, but it turns itself off under prefork - the only MPM mpm-itk runs
++     * with - so it never gets that far. Say so and carry on: refusing to start
++     * would take down servers that merely have the module packaged in. */
++    if (itk_module_loaded("mod_http2.c")) {
++        ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s,
++                     "mod_http2 is loaded; it stays inactive under prefork, so it "
++                     "cannot start threads in the child, but unload it rather than "
++                     "depend on that.");
++    }
++
+     ap_add_version_component(p, "mpm-itk/" MPMITK_VERSION);
+     return OK;
+ }
diff --git a/warn-suid-dumpable.patch b/warn-suid-dumpable.patch
new file mode 100644
index 0000000..e50d3e7
--- /dev/null
+++ b/warn-suid-dumpable.patch
@@ -0,0 +1,34 @@
+--- mpm-itk-2.4.7-04/mpm_itk.c.orig
++++ mpm-itk-2.4.7-04/mpm_itk.c
+@@ -47,6 +47,7 @@
+ #endif
+ #include <pwd.h>
+ #include <grp.h>
++#include <fcntl.h>
+ 
+ #include "ap_config.h"
+ #include "httpd.h"
+@@ -324,6 +325,23 @@
+                      "depend on that.");
+     }
+ 
++    /* Warn if fs.suid_dumpable is in the dangerous debug mode (1). The per-request
++     * child forces itself non-dumpable right after setuid, but those are two
++     * syscalls, so under suid_dumpable=1 a same-uid local attacker could still race
++     * the worker's memory before it is locked down. Keep the sysctl at 0. */
++    {
++        int fd = open("/proc/sys/fs/suid_dumpable", O_RDONLY);
++        if (fd >= 0) {
++            char buf = 0;
++            if (read(fd, &buf, 1) == 1 && buf == '1') {
++                ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s,
++                             "fs.suid_dumpable=1 weakens mpm-itk's memory isolation "
++                             "against same-uid local attackers; set it to 0.");
++            }
++            close(fd);
++        }
++    }
++
+     ap_add_version_component(p, "mpm-itk/" MPMITK_VERSION);
+     return OK;
+ }
================================================================

---- gitweb:

http://git.pld-linux.org/gitweb.cgi/packages/apache-mod_mpm_itk.git/commitdiff/12c7939da51a64098d5ace0eb2fb84c09b09f201



More information about the pld-cvs-commit mailing list