diff --git a/ports/common/guts/execveat.c b/ports/common/guts/execveat.c
new file mode 100644
index 0000000..10e0d52
--- /dev/null
+++ b/ports/common/guts/execveat.c
@@ -0,0 +1,29 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * static int
+ * wrap_execveat(int dirfd, const char *path, char *const *argv, char *const *envp, int flags) {
+ *	int rc = -1;
+ */
+	char **new_environ;
+
+	/* as for execve(), the path is not canonicalized or redirected
+	 * into the chroot environment
+	 */
+	if (antimagic == 0 && path && (path[0] == '/' || dirfd == AT_FDCWD)) {
+		const char *path_guess = pseudo_exec_path(path, 0);
+		pseudo_client_op(OP_EXEC, PSA_EXEC, -1, -1, path_guess, 0);
+	}
+
+	new_environ = pseudo_setupenvp(envp);
+	if (pseudo_has_unload(new_environ))
+		new_environ = pseudo_dropenvp(new_environ);
+
+	sigprocmask(SIG_SETMASK, &pseudo_saved_sigmask, NULL);
+	rc = real_execveat(dirfd, path, argv, new_environ, flags);
+
+	pseudo_free_envp(new_environ);
+
+/*	return rc;
+ * }
+ */
diff --git a/ports/common/guts/execvpe.c b/ports/common/guts/execvpe.c
new file mode 100644
index 0000000..37e4927
--- /dev/null
+++ b/ports/common/guts/execvpe.c
@@ -0,0 +1,29 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * static int
+ * wrap_execvpe(const char *file, char *const *argv, char *const *envp) {
+ *	int rc = -1;
+ */
+	char **new_environ;
+
+	/* as for execvp(), the file is not canonicalized or redirected
+	 * into the chroot environment
+	 */
+	if (antimagic == 0) {
+		const char *path_guess = pseudo_exec_path(file, 1);
+		pseudo_client_op(OP_EXEC, PSA_EXEC, -1, -1, path_guess, 0);
+	}
+
+	new_environ = pseudo_setupenvp(envp);
+	if (pseudo_has_unload(new_environ))
+		new_environ = pseudo_dropenvp(new_environ);
+
+	sigprocmask(SIG_SETMASK, &pseudo_saved_sigmask, NULL);
+	rc = real_execvpe(file, argv, new_environ);
+
+	pseudo_free_envp(new_environ);
+
+/*	return rc;
+ * }
+ */
diff --git a/ports/common/guts/fexecve.c b/ports/common/guts/fexecve.c
new file mode 100644
index 0000000..a0162d3
--- /dev/null
+++ b/ports/common/guts/fexecve.c
@@ -0,0 +1,21 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * static int
+ * wrap_fexecve(int fd, char *const *argv, char *const *envp) {
+ *	int rc = -1;
+ */
+	char **new_environ;
+
+	new_environ = pseudo_setupenvp(envp);
+	if (pseudo_has_unload(new_environ))
+		new_environ = pseudo_dropenvp(new_environ);
+
+	sigprocmask(SIG_SETMASK, &pseudo_saved_sigmask, NULL);
+	rc = real_fexecve(fd, argv, new_environ);
+
+	pseudo_free_envp(new_environ);
+
+/*	return rc;
+ * }
+ */
diff --git a/ports/common/pseudo_wrappers.c b/ports/common/pseudo_wrappers.c
index 2cbc946..2ad0814 100644
--- a/ports/common/pseudo_wrappers.c
+++ b/ports/common/pseudo_wrappers.c
@@ -331,6 +331,120 @@ execvp(const char *file, char *const *argv) {
 	return rc;
 }
 
+int
+execvpe(const char *file, char *const *argv, char *const *envp) {
+	sigset_t saved;
+
+	int rc = -1;
+	PROFILE_START;
+
+	if (!pseudo_check_wrappers() || !real_execvpe) {
+		/* rc was initialized to the "failure" value */
+		pseudo_enosys("execvpe");
+		PROFILE_DONE;
+		return rc;
+	}
+
+	pseudo_debug(PDBGF_WRAPPER, "called: execvpe\n");
+	pseudo_sigblock(&saved);
+	if (pseudo_getlock()) {
+		errno = EBUSY;
+		sigprocmask(SIG_SETMASK, &saved, NULL);
+		PROFILE_DONE;
+		return -1;
+	}
+
+	int save_errno;
+
+	/* exec*() use this to restore the sig mask */
+	pseudo_saved_sigmask = saved;
+	rc = wrap_execvpe(file, argv, envp);
+
+	save_errno = errno;
+	pseudo_droplock();
+	sigprocmask(SIG_SETMASK, &saved, NULL);
+	pseudo_debug(PDBGF_WRAPPER, "completed: execvpe\n");
+	errno = save_errno;
+	PROFILE_DONE;
+	return rc;
+}
+
+int
+fexecve(int fd, char *const *argv, char *const *envp) {
+	sigset_t saved;
+
+	int rc = -1;
+	PROFILE_START;
+
+	if (!pseudo_check_wrappers() || !real_fexecve) {
+		/* rc was initialized to the "failure" value */
+		pseudo_enosys("fexecve");
+		PROFILE_DONE;
+		return rc;
+	}
+
+	pseudo_debug(PDBGF_WRAPPER, "called: fexecve\n");
+	pseudo_sigblock(&saved);
+	if (pseudo_getlock()) {
+		errno = EBUSY;
+		sigprocmask(SIG_SETMASK, &saved, NULL);
+		PROFILE_DONE;
+		return -1;
+	}
+
+	int save_errno;
+
+	/* exec*() use this to restore the sig mask */
+	pseudo_saved_sigmask = saved;
+	rc = wrap_fexecve(fd, argv, envp);
+
+	save_errno = errno;
+	pseudo_droplock();
+	sigprocmask(SIG_SETMASK, &saved, NULL);
+	pseudo_debug(PDBGF_WRAPPER, "completed: fexecve\n");
+	errno = save_errno;
+	PROFILE_DONE;
+	return rc;
+}
+
+int
+execveat(int dirfd, const char *path, char *const *argv, char *const *envp, int flags) {
+	sigset_t saved;
+
+	int rc = -1;
+	PROFILE_START;
+
+	if (!pseudo_check_wrappers() || !real_execveat) {
+		/* rc was initialized to the "failure" value */
+		pseudo_enosys("execveat");
+		PROFILE_DONE;
+		return rc;
+	}
+
+	pseudo_debug(PDBGF_WRAPPER, "called: execveat\n");
+	pseudo_sigblock(&saved);
+	if (pseudo_getlock()) {
+		errno = EBUSY;
+		sigprocmask(SIG_SETMASK, &saved, NULL);
+		PROFILE_DONE;
+		return -1;
+	}
+
+	int save_errno;
+
+	/* exec*() use this to restore the sig mask */
+	pseudo_saved_sigmask = saved;
+	rc = wrap_execveat(dirfd, path, argv, envp, flags);
+
+	save_errno = errno;
+	pseudo_droplock();
+	sigprocmask(SIG_SETMASK, &saved, NULL);
+	pseudo_debug(PDBGF_WRAPPER, "completed: execveat\n");
+	errno = save_errno;
+	PROFILE_DONE;
+	return rc;
+}
+
 /* no profiling in fork because it wouldn't work anyway
  * half the time
  */
@@ -403,6 +517,33 @@ wrap_execvp(const char *file, char *const *argv) {
 }
 
 static int
+wrap_execvpe(const char *file, char *const *argv, char *const *envp) {
+	int rc = -1;
+
+#include "guts/execvpe.c"
+
+	return rc;
+}
+
+static int
+wrap_fexecve(int fd, char *const *argv, char *const *envp) {
+	int rc = -1;
+
+#include "guts/fexecve.c"
+
+	return rc;
+}
+
+static int
+wrap_execveat(int dirfd, const char *path, char *const *argv, char *const *envp, int flags) {
+	int rc = -1;
+
+#include "guts/execveat.c"
+
+	return rc;
+}
+
+static int
 wrap_fork(void) {
 	int rc = -1;
 	
diff --git a/ports/common/wrapfuncs.in b/ports/common/wrapfuncs.in
index 9a44ac1..9b01bf2 100644
--- a/ports/common/wrapfuncs.in
+++ b/ports/common/wrapfuncs.in
@@ -7,3 +7,6 @@ int execvp(const char *file, char *const *argv); /* hand_wrapped=1 */
 int fork(void); /* hand_wrapped=1 */
 int posix_spawn(pid_t *pid, const char *path, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const *argv, char *const *envp); /* hand_wrapped=1 */
 int posix_spawnp(pid_t *pid, const char *file, const posix_spawn_file_actions_t *file_actions, const posix_spawnattr_t *attrp, char *const *argv, char *const *envp); /* hand_wrapped=1 */
+int execvpe(const char *file, char *const *argv, char *const *envp); /* hand_wrapped=1 */
+int fexecve(int fd, char *const *argv, char *const *envp); /* hand_wrapped=1 */
+int execveat(int dirfd, const char *path, char *const *argv, char *const *envp, int flags); /* hand_wrapped=1 */
diff --git a/test/test-exec.c b/test/test-exec.c
new file mode 100644
index 0000000..82ceddf
--- /dev/null
+++ b/test/test-exec.c
@@ -0,0 +1,50 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-only
+ *
+ * Run "/usr/bin/env A=A B=B C=C" through an exec function.
+ *
+ * usage: test-exec [execl|execvpe|fexecve|execveat [VAR=value...]]
+ *
+ * execl() keeps the caller's environment; the others pass only the
+ * VAR=value arguments given, so pseudo has to add its own variables back
+ * (or leave them out, if PSEUDO_UNLOAD is among them).
+ */
+#define _GNU_SOURCE
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+int main(int argc, char **argv) {
+	char *args[] = { "env", "A=A", "B=B", "C=C", NULL };
+	/* argv[2] onwards, followed by argv's terminating NULL */
+	char **envp = argc > 2 ? argv + 2 : argv + argc;
+	const char *fn = argc > 1 ? argv[1] : "execl";
+	int fd;
+
+	if (!strcmp(fn, "execl")) {
+		return execl("/usr/bin/env", "/usr/bin/env", "A=A", "B=B", "C=C", NULL);
+	} else if (!strcmp(fn, "execvpe")) {
+		/* searched for in the caller's PATH */
+		execvpe("env", args, envp);
+	} else if (!strcmp(fn, "fexecve")) {
+		fd = open("/usr/bin/env", O_RDONLY | O_CLOEXEC);
+		if (fd == -1) {
+			perror("/usr/bin/env");
+			return 2;
+		}
+		fexecve(fd, args, envp);
+	} else if (!strcmp(fn, "execveat")) {
+		fd = open("/usr/bin", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+		if (fd == -1) {
+			perror("/usr/bin");
+			return 2;
+		}
+		execveat(fd, "env", args, envp, 0);
+	} else {
+		fprintf(stderr, "unknown function '%s'\n", fn);
+		return 2;
+	}
+	perror(fn);
+	return 2;
+}
diff --git a/test/test-exec.sh b/test/test-exec.sh
new file mode 100755
index 0000000..4308631
--- /dev/null
+++ b/test/test-exec.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: LGPL-2.1-only
+#
+# Run env through each exec function pseudo wraps.  The arguments must
+# get through, and for the functions given an explicit environment, an
+# empty one must get libpseudo back in LD_PRELOAD (as with env -i), and
+# one with PSEUDO_UNLOAD must not.
+
+test_exec="$(dirname "$0")/test-exec"
+
+"$test_exec" | grep -q "C=C" || { echo "FAILED: execl"; exit 1; }
+
+rc=0
+for fn in execvpe fexecve execveat; do
+    out=$("$test_exec" $fn)
+    if ! grep -q "C=C" <<< "$out"; then
+        echo "FAILED: $fn: arguments not passed"
+        rc=1
+    fi
+    if ! grep -q "^LD_PRELOAD=.*libpseudo" <<< "$out"; then
+        echo "FAILED: $fn: pseudo not restored to LD_PRELOAD"
+        rc=1
+    fi
+    out=$("$test_exec" $fn PSEUDO_UNLOAD=1)
+    if grep -q "^LD_PRELOAD=.*libpseudo" <<< "$out"; then
+        echo "FAILED: $fn: PSEUDO_UNLOAD did not drop pseudo"
+        rc=1
+    fi
+done
+exit $rc
diff --git a/test/test-execl.c b/test/test-execl.c
deleted file mode 100644
index 31b803c..0000000
--- a/test/test-execl.c
+++ /dev/null
@@ -1,4 +0,0 @@
-#include <unistd.h>
-int main() {
-        return execl("/usr/bin/env", "/usr/bin/env", "A=A", "B=B", "C=C", NULL);
-}
diff --git a/test/test-execl.sh b/test/test-execl.sh
deleted file mode 100755
index 8715822..0000000
--- a/test/test-execl.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-#
-# SPDX-License-Identifier: LGPL-2.1-only
-#
-$(dirname "$0")/test-execl | grep -q "C=C"
-
-if [ "$?" = "0" ]
-then
-    #echo "Passed."
-    exit 0
-fi
-#echo "Failed"
-exit 1
