mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Brian Witte <brianwitte@mailfence.com>
To: Kees Cook <kees@kernel.org>
Cc: Brian Witte <brianwitte@mailfence.com>,
	Alexander Viro <viro@zeniv.linux.org.uk>,
	Christian Brauner <brauner@kernel.org>, Jan Kara <jack@suse.cz>,
	Lorenzo Stoakes <ljs@kernel.org>, Jann Horn <jannh@google.com>,
	Helge Deller <deller@gmx.de>,
	Justin Stitt <justinstitt@google.com>,
	Daniel Micay <danielmicay@gmail.com>,
	Shuah Khan <shuah@kernel.org>,
	linux-fsdevel@vger.kernel.org, linux-mm@kvack.org,
	linux-hardening@vger.kernel.org, linux-kselftest@vger.kernel.org,
	linux-kernel@vger.kernel.org
Subject: [PATCH 2/2] selftests/exec: test the argument block's in-page offset
Date: Fri, 23 Oct 2026 03:54:46 +0200 (CEST)	[thread overview]
Message-ID: <20261023015345.1594514-3-brianwitte@mailfence.com> (raw)
In-Reply-To: <20261023015345.1594514-1-brianwitte@mailfence.com>

Exec a helper, argblock-report, 64 times per case. Each case checks that
the in-page offset of argv[0] varies exactly when the stack top chosen
by the ELF loader does:
 - under ADDR_NO_RANDOMIZE: fixed;
 - by default: random;
 - a set-user-ID copy under ADDR_NO_RANDOMIZE: random;
 - that copy on a nosuid mount, under no_new_privs, or in a new user
   namespace where its owner has no mapping: fixed.

The tests use the kselftest harness. The fixture keeps the copy in a
private temporary directory and removes it on teardown. The copy is
owned by the caller, so no privilege is needed.

Link: https://github.com/KSPP/linux/issues/283
Signed-off-by: Brian Witte <brianwitte@mailfence.com>
---
 tools/testing/selftests/exec/.gitignore       |   3 +
 tools/testing/selftests/exec/Makefile         |   5 +
 .../testing/selftests/exec/argblock-offset.c  | 214 ++++++++++++++++++
 .../testing/selftests/exec/argblock-report.c  |  24 ++
 4 files changed, 246 insertions(+)
 create mode 100644 tools/testing/selftests/exec/argblock-offset.c
 create mode 100644 tools/testing/selftests/exec/argblock-report.c

diff --git a/tools/testing/selftests/exec/.gitignore b/tools/testing/selftests/exec/.gitignore
index e42ecd4c908d..991097f7fc43 100644
--- a/tools/testing/selftests/exec/.gitignore
+++ b/tools/testing/selftests/exec/.gitignore
@@ -9,6 +9,9 @@ execveat.ephemeral
 execveat.denatured
 non-regular
 null-argv
+/argblock-offset
+/argblock-offset.*
+/argblock-report
 /check-exec
 /false
 /inc
diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile
index 91decfa8ff2c..4835c7ca4a20 100644
--- a/tools/testing/selftests/exec/Makefile
+++ b/tools/testing/selftests/exec/Makefile
@@ -21,6 +21,11 @@ TEST_GEN_PROGS += recursion-depth
 TEST_GEN_PROGS += null-argv
 TEST_GEN_PROGS += check-exec
 
+# The argument block's in-page offset must be random exactly when the
+# ELF loader randomizes the stack. Unprivileged.
+TEST_GEN_PROGS += argblock-offset
+TEST_GEN_FILES += argblock-report
+
 # binfmt_misc must not be reachable as an exec source or as a stacking layer,
 # or an 'F' entry can pin the instance that owns it. Unprivileged, no bpf.
 TEST_GEN_PROGS += binfmt_misc_selfpin
diff --git a/tools/testing/selftests/exec/argblock-offset.c b/tools/testing/selftests/exec/argblock-offset.c
new file mode 100644
index 000000000000..6edf9e6aadaf
--- /dev/null
+++ b/tools/testing/selftests/exec/argblock-offset.c
@@ -0,0 +1,214 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * The argument block should land at a random offset within its page
+ * exactly when the ELF loader randomizes the stack: always, unless
+ * ADDR_NO_RANDOMIZE is set and the exec is not set-id. Each case
+ * execs argblock-report repeatedly and compares both.
+ *
+ * Runs unprivileged: the set-user-ID copy is owned by the caller and
+ * kept in a private directory.
+ */
+#define _GNU_SOURCE
+#include <sched.h>
+#include <sys/personality.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/statvfs.h>
+
+#include "binfmt_misc_common.h"	/* artifact_path(), copy_file() */
+#include "kselftest_harness.h"
+
+/*
+ * A page holds at least 4088 possible offsets, so 64 samples all landing
+ * on one value happen by chance with probability at most (1/4088)^63.
+ */
+#define SAMPLES 64
+
+/* How the helper is exec'd. */
+#define NO_RAND	0x1	/* under ADDR_NO_RANDOMIZE */
+#define NNP	0x2	/* under no_new_privs */
+#define USERNS	0x4	/* in a new user namespace with no mappings */
+
+FIXTURE(argblock) {
+	char report[PATH_MAX];	/* the helper */
+	char dir[32];		/* private directory for the set-id copy */
+	char setid[PATH_MAX];	/* set-user-ID copy of the helper */
+	bool have_dir, have_setid, nosuid;
+	int randomize_va_space;
+};
+
+static int read_randomize_va_space(void)
+{
+	FILE *f = fopen("/proc/sys/kernel/randomize_va_space", "r");
+	int v = -1;
+
+	if (f) {
+		if (fscanf(f, "%d", &v) != 1)
+			v = -1;
+		fclose(f);
+	}
+	return v;
+}
+
+FIXTURE_SETUP(argblock)
+{
+	struct statvfs sv;
+
+	ASSERT_EQ(artifact_path(self->report, sizeof(self->report),
+				"argblock-report"), 0);
+	self->randomize_va_space = read_randomize_va_space();
+
+	/* A private directory keeps the set-user-ID copy out of reach. */
+	strcpy(self->dir, "argblock-offset.XXXXXX");
+	self->have_dir = mkdtemp(self->dir);
+	if (!self->have_dir)
+		return;
+	snprintf(self->setid, sizeof(self->setid), "%s/setid", self->dir);
+	self->have_setid = !copy_file(self->report, self->setid) &&
+			   !chmod(self->setid, 04755);
+	self->nosuid = !statvfs(self->dir, &sv) && (sv.f_flag & ST_NOSUID);
+}
+
+FIXTURE_TEARDOWN(argblock)
+{
+	if (!self->have_dir)
+		return;
+	unlink(self->setid);
+	rmdir(self->dir);
+}
+
+struct sample {
+	unsigned long offset;		/* argv[0] within its page */
+	unsigned long stack_top;	/* end of [stack], chosen by the loader */
+};
+
+static int sample(const char *path, int how, struct sample *s)
+{
+	int pfd[2], status;
+	char buf[64];
+	ssize_t n;
+	pid_t pid;
+
+	if (pipe(pfd))
+		return -1;
+	pid = fork();
+	if (pid < 0)
+		return -1;
+	if (pid == 0) {
+		char *argv[] = { (char *)path, NULL };
+		char *envp[] = { NULL };
+
+		if ((how & NO_RAND) && personality(ADDR_NO_RANDOMIZE) < 0)
+			_exit(2);
+		if ((how & NNP) && prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
+			_exit(2);
+		if ((how & USERNS) && unshare(CLONE_NEWUSER))
+			_exit(2);
+		dup2(pfd[1], STDOUT_FILENO);
+		execve(path, argv, envp);
+		_exit(3);
+	}
+	close(pfd[1]);
+	n = read(pfd[0], buf, sizeof(buf) - 1);
+	close(pfd[0]);
+	waitpid(pid, &status, 0);
+	if (n <= 0 || !WIFEXITED(status) || WEXITSTATUS(status))
+		return -1;
+	buf[n] = '\0';
+	return sscanf(buf, "%lx %lx", &s->offset, &s->stack_top) == 2 ? 0 : -1;
+}
+
+/*
+ * Exec @path SAMPLES times. The in-page offset must vary as @expect says,
+ * and vary exactly when the loader's stack top does.
+ */
+static void check(struct __test_metadata *_metadata, const char *path,
+		  int how, bool expect)
+{
+	bool offset_varies = false, stack_varies = false;
+	struct sample first, s;
+	int i;
+
+	for (i = 0; i < SAMPLES; i++) {
+		ASSERT_EQ(sample(path, how, &s), 0)
+			TH_LOG("exec of %s failed", path);
+		if (!i)
+			first = s;
+		offset_varies |= s.offset != first.offset;
+		stack_varies |= s.stack_top != first.stack_top;
+	}
+	EXPECT_EQ(stack_varies, expect)
+		TH_LOG("the loader %s the stack",
+		       stack_varies ? "randomized" : "did not randomize");
+	EXPECT_EQ(offset_varies, expect)
+		TH_LOG("the argument block offset is %s",
+		       offset_varies ? "random" : "fixed");
+}
+
+/* Can this process create a user namespace? */
+static bool have_userns(void)
+{
+	int status;
+	pid_t pid = fork();
+
+	if (pid == 0)
+		_exit(unshare(CLONE_NEWUSER) ? 1 : 0);
+	return pid > 0 && waitpid(pid, &status, 0) == pid &&
+	       WIFEXITED(status) && !WEXITSTATUS(status);
+}
+
+/* Without privilege, ADDR_NO_RANDOMIZE keeps the offset fixed. */
+TEST_F(argblock, addr_no_randomize_keeps_offset_fixed)
+{
+	check(_metadata, self->report, NO_RAND, false);
+}
+
+/* By default the offset is random. */
+TEST_F(argblock, offset_is_random)
+{
+	if (self->randomize_va_space <= 0)
+		SKIP(return, "randomize_va_space is 0");
+	check(_metadata, self->report, 0, true);
+}
+
+/* A set-id exec clears ADDR_NO_RANDOMIZE, so the offset is random again. */
+TEST_F(argblock, setid_ignores_addr_no_randomize)
+{
+	if (self->randomize_va_space <= 0)
+		SKIP(return, "randomize_va_space is 0");
+	if (!self->have_setid)
+		SKIP(return, "cannot create a set-user-ID copy");
+	if (self->nosuid)
+		SKIP(return, "set-user-ID copy is on a nosuid mount");
+	check(_metadata, self->setid, NO_RAND, true);
+}
+
+/* On a nosuid mount the exec gains nothing, so the flag holds. */
+TEST_F(argblock, setid_on_nosuid_keeps_offset_fixed)
+{
+	if (!self->have_setid)
+		SKIP(return, "cannot create a set-user-ID copy");
+	if (!self->nosuid)
+		SKIP(return, "set-user-ID copy is not on a nosuid mount");
+	check(_metadata, self->setid, NO_RAND, false);
+}
+
+/* no_new_privs makes the exec ignore the set-id bit. */
+TEST_F(argblock, setid_under_no_new_privs_keeps_offset_fixed)
+{
+	if (!self->have_setid)
+		SKIP(return, "cannot create a set-user-ID copy");
+	check(_metadata, self->setid, NO_RAND | NNP, false);
+}
+
+/* The copy's owner has no mapping there, so the set-id bit is ignored. */
+TEST_F(argblock, setid_in_user_namespace_keeps_offset_fixed)
+{
+	if (!self->have_setid)
+		SKIP(return, "cannot create a set-user-ID copy");
+	if (!have_userns())
+		SKIP(return, "cannot create a user namespace");
+	check(_metadata, self->setid, NO_RAND | USERNS, false);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/exec/argblock-report.c b/tools/testing/selftests/exec/argblock-report.c
new file mode 100644
index 000000000000..45744ec9bda9
--- /dev/null
+++ b/tools/testing/selftests/exec/argblock-report.c
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Helper for argblock-offset: print the in-page offset of argv[0] and the
+ * end of the [stack] VMA, which the ELF loader chooses.
+ */
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+int main(int argc, char **argv)
+{
+	unsigned long end = 0;
+	char line[256];
+	FILE *maps = fopen("/proc/self/maps", "r");
+
+	while (maps && fgets(line, sizeof(line), maps))
+		if (strstr(line, "[stack]"))
+			sscanf(line, "%*[0-9a-f]-%lx", &end);
+	if (argc < 1 || !end)
+		return 1;
+	printf("%lx %lx\n", (unsigned long)argv[0] & (sysconf(_SC_PAGESIZE) - 1),
+	       end);
+	return 0;
+}
-- 
2.47.3


      parent reply	other threads:[~2026-10-23  1:54 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-23  1:54 [PATCH 0/2] exec: randomize " Brian Witte
2026-10-23  1:54 ` [PATCH 1/2] exec: randomize the argument block's offset within its page Brian Witte
2026-10-23  1:54 ` Brian Witte [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261023015345.1594514-3-brianwitte@mailfence.com \
    --to=brianwitte@mailfence.com \
    --cc=brauner@kernel.org \
    --cc=danielmicay@gmail.com \
    --cc=deller@gmx.de \
    --cc=jack@suse.cz \
    --cc=jannh@google.com \
    --cc=justinstitt@google.com \
    --cc=kees@kernel.org \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-hardening@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=ljs@kernel.org \
    --cc=shuah@kernel.org \
    --cc=viro@zeniv.linux.org.uk \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®