[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