Fix execvpe ENOEXEC behavior.
The special case for absolute paths wasn't handling ENOEXEC.
Also add more extensive tests for execvpe.
Also switch to manually doing the fork in ExecTestHelper::Run because
ASSERT_EXIT doesn't actually return, meaning we were only running the
first part of each test.
Bug: http://b/31073104
Change-Id: I7a4640afc6d290c51ba2e66fc1b9bb6b0fc174f7
diff --git a/tests/unistd_test.cpp b/tests/unistd_test.cpp
index 31f52a5..69d1906 100644
--- a/tests/unistd_test.cpp
+++ b/tests/unistd_test.cpp
@@ -23,6 +23,7 @@
#include <errno.h>
#include <fcntl.h>
+#include <libgen.h>
#include <limits.h>
#include <stdint.h>
#include <sys/capability.h>
@@ -1085,9 +1086,34 @@
void Run(const std::function<void ()>& child_fn,
int expected_exit_status,
const char* expected_output) {
- ASSERT_EXIT({ dup2(STDERR_FILENO, STDOUT_FILENO); child_fn(); },
- ::testing::ExitedWithCode(expected_exit_status),
- expected_output);
+ int fds[2];
+ ASSERT_NE(pipe2(fds, 0), -1);
+
+ pid_t pid = fork();
+ ASSERT_NE(pid, -1);
+
+ if (pid == 0) {
+ // Child.
+ close(fds[0]);
+ dup2(fds[1], STDOUT_FILENO);
+ dup2(fds[1], STDERR_FILENO);
+ if (fds[1] != STDOUT_FILENO && fds[1] != STDERR_FILENO) close(fds[1]);
+ child_fn();
+ FAIL();
+ }
+
+ // Parent.
+ close(fds[1]);
+ std::string output;
+ char buf[BUFSIZ];
+ ssize_t bytes_read;
+ while ((bytes_read = TEMP_FAILURE_RETRY(read(fds[0], buf, sizeof(buf)))) > 0) {
+ output.append(buf, bytes_read);
+ }
+ close(fds[0]);
+
+ AssertChildExited(pid, expected_exit_status);
+ ASSERT_EQ(expected_output, output);
}
private:
@@ -1108,7 +1134,7 @@
ASSERT_EQ(EACCES, errno);
}
-TEST(UNISTD_TEST, execve) {
+TEST(UNISTD_TEST, execve_args) {
// int execve(const char* path, char* argv[], char* envp[]);
// Test basic argument passing.
@@ -1176,6 +1202,7 @@
TEST(UNISTD_TEST, execvp_failure) {
ExecTestHelper eth;
+ eth.SetArgs({nullptr});
errno = 0;
ASSERT_EQ(-1, execvp("/", eth.GetArgs()));
ASSERT_EQ(EACCES, errno);
@@ -1208,3 +1235,40 @@
eth.SetEnv({"A=B", nullptr});
eth.Run([&]() { execvpe("printenv", eth.GetArgs(), eth.GetEnv()); }, 0, "A=B\n");
}
+
+TEST(UNISTD_TEST, execvpe_ENOEXEC) {
+ // Create a shell script with #!.
+ TemporaryFile tf;
+ ASSERT_TRUE(android::base::WriteStringToFile("#!" BIN_DIR "sh\necho script\n", tf.filename));
+
+ // Set $PATH so we can find it.
+ setenv("PATH", dirname(tf.filename), 1);
+
+ ExecTestHelper eth;
+ eth.SetArgs({basename(tf.filename), nullptr});
+
+ // It's not inherently executable.
+ errno = 0;
+ ASSERT_EQ(-1, execvpe(basename(tf.filename), eth.GetArgs(), eth.GetEnv()));
+ ASSERT_EQ(EACCES, errno);
+
+ // Make it executable.
+ ASSERT_EQ(0, chmod(tf.filename, 0555));
+
+ // TemporaryFile will have a writable fd, so we can test ETXTBSY while we're here...
+ errno = 0;
+ ASSERT_EQ(-1, execvpe(basename(tf.filename), eth.GetArgs(), eth.GetEnv()));
+ ASSERT_EQ(ETXTBSY, errno);
+
+ // 1. The simplest test: the kernel should handle this.
+ ASSERT_EQ(0, close(tf.fd));
+ eth.Run([&]() { execvpe(basename(tf.filename), eth.GetArgs(), eth.GetEnv()); }, 0, "script\n");
+
+ // 2. Try again without a #!. We should have to handle this ourselves.
+ ASSERT_TRUE(android::base::WriteStringToFile("echo script\n", tf.filename));
+ eth.Run([&]() { execvpe(basename(tf.filename), eth.GetArgs(), eth.GetEnv()); }, 0, "script\n");
+
+ // 3. Again without a #!, but also with a leading '/', since that's a special case in the
+ // implementation.
+ eth.Run([&]() { execvpe(tf.filename, eth.GetArgs(), eth.GetEnv()); }, 0, "script\n");
+}