I had cause to look at the vfork() support for GCS and realised that we don't have any direct test coverage, this series does so by adding vfork() to nolibc and then using that in basic-gcs to provide some simple vfork() coverage.
Signed-off-by: Mark Brown broonie@kernel.org --- Mark Brown (2): tools/nolibc: Provide vfork() kselftest/arm64: Add a test for vfork() with GCS
tools/include/nolibc/sys.h | 29 ++++++++++++ tools/testing/selftests/arm64/gcs/basic-gcs.c | 63 +++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) --- base-commit: 19272b37aa4f83ca52bdf9c16d5d81bdd1354494 change-id: 20250528-arm64-gcs-vfork-exit-4a7daf7652ee
Best regards,
To allow testing of vfork() support in the arm64 basic-gcs test provide an implementation for nolibc, using the vfork() syscall if one is available and otherwise clone3(). We implement in terms of clone3() since the order of the arguments for clone() varies between architectures.
Signed-off-by: Mark Brown broonie@kernel.org --- tools/include/nolibc/sys.h | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+)
diff --git a/tools/include/nolibc/sys.h b/tools/include/nolibc/sys.h index 9556c69a6ae1..e056da010f64 100644 --- a/tools/include/nolibc/sys.h +++ b/tools/include/nolibc/sys.h @@ -22,6 +22,7 @@ #include <linux/time.h> #include <linux/auxvec.h> #include <linux/fcntl.h> /* for O_* and AT_* */ +#include <linux/sched.h> /* for clone_args */ #include <linux/stat.h> /* for statx() */
#include "errno.h" @@ -340,6 +341,34 @@ pid_t fork(void) return __sysret(sys_fork()); }
+#ifndef sys_vfork +static __attribute__((unused)) +pid_t sys_vfork(void) +{ +#ifdef __NR_vfork + return my_syscall0(__NR_vfork); +#elif defined(__NR_clone3) + /* + * clone() could be used but has different argument orders per + * architecture. + */ + struct clone_args args = { + .flags = CLONE_VM | CLONE_VFORK, + .exit_signal = SIGCHLD, + }; + + return my_syscall2(__NR_clone3, &args, sizeof(args)); +#else + return __nolibc_enosys(__func__); +#endif +} +#endif + +static __attribute__((unused)) +pid_t vfork(void) +{ + return __sysret(sys_vfork()); +}
/* * int fsync(int fd);
Hi Mark,
On 2025-06-09 16:08:56+0100, Mark Brown wrote:
To allow testing of vfork() support in the arm64 basic-gcs test provide an implementation for nolibc, using the vfork() syscall if one is available and otherwise clone3(). We implement in terms of clone3() since the order of the arguments for clone() varies between architectures.
Thanks for the patch!
Do you want to take this series through your tree?
Signed-off-by: Mark Brown broonie@kernel.org
tools/include/nolibc/sys.h | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+)
diff --git a/tools/include/nolibc/sys.h b/tools/include/nolibc/sys.h index 9556c69a6ae1..e056da010f64 100644 --- a/tools/include/nolibc/sys.h +++ b/tools/include/nolibc/sys.h @@ -22,6 +22,7 @@ #include <linux/time.h> #include <linux/auxvec.h> #include <linux/fcntl.h> /* for O_* and AT_* */ +#include <linux/sched.h> /* for clone_args */ #include <linux/stat.h> /* for statx() */ #include "errno.h" @@ -340,6 +341,34 @@ pid_t fork(void) return __sysret(sys_fork()); } +#ifndef sys_vfork +static __attribute__((unused)) +pid_t sys_vfork(void) +{ +#ifdef __NR_vfork
- return my_syscall0(__NR_vfork);
+#elif defined(__NR_clone3)
- /*
* clone() could be used but has different argument orders per
* architecture.
*/
- struct clone_args args = {
.flags = CLONE_VM | CLONE_VFORK,
.exit_signal = SIGCHLD,
- };
- return my_syscall2(__NR_clone3, &args, sizeof(args));
+#else
- return __nolibc_enosys(__func__);
+#endif +} +#endif
+static __attribute__((unused)) +pid_t vfork(void) +{
- return __sysret(sys_vfork());
+}
Could you also add a test to nolibc-test.c? Maybe extend test_fork() with a flag to use either fork() or vfork(). And maybe change the exit() in the test to _exit(); not that it would make a difference for nolibc (yet).
/*
- int fsync(int fd);
-- 2.39.5
Small followup review, sorry for the noise.
On 2025-06-09 16:08:56+0100, Mark Brown wrote: <snip>
+#ifndef sys_vfork
This ifndef is not necessary here. No architecture has a special version.
+static __attribute__((unused)) +pid_t sys_vfork(void) +{ +#ifdef __NR_vfork
For consistency: #if defined(__NR_vfork)
- return my_syscall0(__NR_vfork);
+#elif defined(__NR_clone3)
- /*
* clone() could be used but has different argument orders per
* architecture.
*/
- struct clone_args args = {
.flags = CLONE_VM | CLONE_VFORK,
.exit_signal = SIGCHLD,
- };
- return my_syscall2(__NR_clone3, &args, sizeof(args));
+#else
- return __nolibc_enosys(__func__);
+#endif +} +#endif
On Mon, Jun 09, 2025 at 05:34:33PM +0200, Thomas Weißschuh wrote:
On 2025-06-09 16:08:56+0100, Mark Brown wrote:
+#ifndef sys_vfork
This ifndef is not necessary here. No architecture has a special version.
Ah, I was adding it defensively in case it was needed in future.
+static __attribute__((unused)) +pid_t sys_vfork(void) +{ +#ifdef __NR_vfork
For consistency: #if defined(__NR_vfork)
Are you sure? I'm seeing an awful lot of #ifdef __NR_foo instances in sys.h - it looks like the idiom is to use that unless it either needs an additional condition (with || or &&) or is an elif.
On 2025-06-09 16:43:55+0100, Mark Brown wrote:
On Mon, Jun 09, 2025 at 05:34:33PM +0200, Thomas Weißschuh wrote:
On 2025-06-09 16:08:56+0100, Mark Brown wrote:
<snip>
+static __attribute__((unused)) +pid_t sys_vfork(void) +{ +#ifdef __NR_vfork
For consistency: #if defined(__NR_vfork)
Are you sure? I'm seeing an awful lot of #ifdef __NR_foo instances in sys.h - it looks like the idiom is to use that unless it either needs an additional condition (with || or &&) or is an elif.
In sys.h, indeed. These have mostly grown historically, newer ones use #if defined() together with #elif defined().
On Mon, Jun 09, 2025 at 05:51:19PM +0200, Thomas Weißschuh wrote:
On 2025-06-09 16:43:55+0100, Mark Brown wrote:
On Mon, Jun 09, 2025 at 05:34:33PM +0200, Thomas Weißschuh wrote:
For consistency: #if defined(__NR_vfork)
Are you sure? I'm seeing an awful lot of #ifdef __NR_foo instances in sys.h - it looks like the idiom is to use that unless it either needs an additional condition (with || or &&) or is an elif.
In sys.h, indeed. These have mostly grown historically, newer ones use #if defined() together with #elif defined().
There aren't actually any of the newer ones in sys.h! I guess I'll add a patch doing the style change...
Ensure that we've got at least some coverage of the special cases around vfork() by adding a test case in basic-gcs doing the same thing as the plain fork() one - vfork(), do a few checks and then return to the parent.
Signed-off-by: Mark Brown broonie@kernel.org --- tools/testing/selftests/arm64/gcs/basic-gcs.c | 63 +++++++++++++++++++++++++++ 1 file changed, 63 insertions(+)
diff --git a/tools/testing/selftests/arm64/gcs/basic-gcs.c b/tools/testing/selftests/arm64/gcs/basic-gcs.c index 3fb9742342a3..96ea51cf7163 100644 --- a/tools/testing/selftests/arm64/gcs/basic-gcs.c +++ b/tools/testing/selftests/arm64/gcs/basic-gcs.c @@ -298,6 +298,68 @@ static bool test_fork(void) return pass; }
+/* A vfork()ed process can run and exit */ +static bool test_vfork(void) +{ + unsigned long child_mode; + int ret, status; + pid_t pid; + bool pass = true; + + pid = vfork(); + if (pid == -1) { + ksft_print_msg("vfork() failed: %d\n", errno); + pass = false; + goto out; + } + if (pid == 0) { + /* In child, make sure we can call a function, read + * the GCS pointer and status and then exit */ + valid_gcs_function(); + get_gcspr(); + + ret = my_syscall5(__NR_prctl, PR_GET_SHADOW_STACK_STATUS, + &child_mode, 0, 0, 0); + if (ret == 0 && !(child_mode & PR_SHADOW_STACK_ENABLE)) { + ksft_print_msg("GCS not enabled in child\n"); + ret = -EINVAL; + } + + exit(ret); + } + + /* + * In parent, check we can still do function calls then block + * for the child. + */ + valid_gcs_function(); + + ksft_print_msg("Waiting for child %d\n", pid); + + ret = waitpid(pid, &status, 0); + if (ret == -1) { + ksft_print_msg("Failed to wait for child: %d\n", + errno); + return false; + } + + if (!WIFEXITED(status)) { + ksft_print_msg("Child exited due to signal %d\n", + WTERMSIG(status)); + pass = false; + } else { + if (WEXITSTATUS(status)) { + ksft_print_msg("Child exited with status %d\n", + WEXITSTATUS(status)); + pass = false; + } + } + +out: + + return pass; +} + typedef bool (*gcs_test)(void);
static struct { @@ -314,6 +376,7 @@ static struct { { "enable_invalid", enable_invalid, true }, { "map_guarded_stack", map_guarded_stack }, { "fork", test_fork }, + { "vfork", test_vfork }, };
int main(void)
linux-kselftest-mirror@lists.linaro.org