/******************************************************************************* * Copyright (c) 2011 Arapiki Solutions Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * "Peter Smith <psmith@arapiki.com>" - initial API and * implementation and/or initial documentation *******************************************************************************/ package com.buildml.scanner.legacy; import static org.junit.Assert.*; import java.io.File; import org.junit.After; import org.junit.Before; import org.junit.Test; import com.buildml.model.CommonTestUtils; import com.buildml.model.IActionMgr; import com.buildml.model.IActionMgr.OperationType; import com.buildml.model.IBuildStore; import com.buildml.model.IFileMgr; import com.buildml.utils.errors.ErrorCode; import com.buildml.utils.os.SystemUtils; import com.buildml.utils.string.PathUtils; /** * Basic testing that the LegacyBuildScanner can produce a valid * BuildStore. There are many test cases, split over multiple * test case files, with this file testing C Functions that * perform an exec()-like operation. * * @author "Peter Smith <psmith@arapiki.com>" */ public class TestCFuncExec { /* variables used in many test cases */ private IBuildStore bs = null; private IActionMgr actionMgr = null; private IFileMgr fileMgr = null; private int shellActionId; /** temporary directory into which test cases can store files */ private File tmpDir; /*=====================================================================================* * Helper methods *=====================================================================================*/ /** * Called before each test case starts. Creates a temporary directory in which the * test case can store temporary files. * @throws Exception */ @Before public void setUp() throws Exception { tmpDir = SystemUtils.createTempDir(); } /*-------------------------------------------------------------------------------------*/ /** * Called after each test case ends. Removes the temporary directory and its content. * @throws Exception */ @After public void tearDown() throws Exception { SystemUtils.deleteDirectory(tmpDir); } /*-------------------------------------------------------------------------------------*/ /** * A helper method for tracing a program that uses exec()-like system calls to * invoke a sub-process. The sub-process (hard-coded in this function) checks * environment variables and command line arguments to determine whether the * exec() call operated correctly. * * @param extraLines The lines of code that perform the exec()-like function. * @param needFork Should we fork() before running the exec()-like function? * @throws Exception */ private void traceExecProgram(String extraLines, boolean needFork) throws Exception { /* * This is the source code for the child program. It validates the environment * variables and command line arguments, returning 123 on success, or some * other number if the variables/arguments are wrong. * Also, if successful create /tmp/flag-file3 as a way for our unit tests to * double-check that the process executed. */ String childSource = "#include <stdio.h>\n" + "#include <stdlib.h>\n" + "int main(int argc, char *argv[]) {" + "char *value = getenv(\"MY_VAR_1\");" + "if ((value == NULL) || (strcmp(value, \"value1\") != 0)){ return 3; }" + "value = getenv(\"SECOND_VARIABLE\");" + "if ((value == NULL) || (strcmp(value, \"13579\") != 0)){ return 4; }" + "if (argc != 4){ return 5; }" + "if (strcmp(argv[1], \"arg1\") != 0){ return 6; }" + "if (strcmp(argv[2], \"arg2\") != 0){ return 7; }" + "if (strcmp(argv[3], \"arg3\") != 0){ return 8; }" + "creat(\"/tmp/flag-file3\", 0666);"+ "return 123;}"; /* compile this hard-coded child program */ String childPath = BuildScannersCommonTestUtils.compileProgram(tmpDir, "child", childSource); /* Substitute the child's path into the extraLines that the caller provided */ extraLines = extraLines.replaceAll("INSERT_PATH", childPath); /* * The parent process sets up the environment variables and command line arguments to be * passed to the child process. Also, write to /tmp/flag-file1 and /tmp/flag-file2 as * a means of indicating to our test cases that progress is being made. */ String parentSource = "#include <fcntl.h>\n" + "#include <stdlib.h>\n" + "#include <unistd.h>\n" + "#include <sys/wait.h>\n" + "extern char **environ;" + "char *tmp_argv[] = {\"child\", \"arg1\", \"arg2\", \"arg3\", 0};"+ "int main() {" + " int status;" + " setenv(\"MY_VAR_1\", \"value1\", 1);" + " setenv(\"SECOND_VARIABLE\", \"13579\", 1);" + " creat(\"/tmp/flag-file1\", 0666);"; /* * If the exec()-like function expects to be run within the child process, * we must fork() first. For functions like posix_spawn(), we don't need * to fork(). */ if (needFork) { parentSource += " switch (fork()) {" + " case 0:" + " " + extraLines + " exit(1);" + " case -1:" + " exit(2);" + " default:" + " wait(&status);" + " if (WEXITSTATUS(status) == 123) { creat(\"/tmp/flag-file2\", 0666); }" + " }" + " return 0;" + "}"; } else { parentSource += " " + extraLines + " return 0;" + "};"; } /* * Run the parent program, and trace the parent and child behaviour into a BuildStore. */ bs = BuildScannersCommonTestUtils.parseLegacyProgram(tmpDir, parentSource, null); /* fetch references to sub objects */ actionMgr = bs.getActionMgr(); fileMgr = bs.getFileMgr(); /* find the root action */ int rootAction = actionMgr.getRootAction("root"); /* * There should only be one top-level action (parent), and it should contain a * single second-level action (child). The second-level action has no children. */ Integer actions[] = actionMgr.getChildren(rootAction); assertEquals(1, actions.length); int parentActionId = actions[0].intValue(); actions = actionMgr.getChildren(parentActionId); assertEquals(1, actions.length); int childActionId = actions[0].intValue(); actions = actionMgr.getChildren(childActionId); assertEquals(0, actions.length); /* * Now validate the file access patterns. The parent action wrote to /tmp/flag-file1 * and /tmp/flag-file2. */ Integer[] fileWrites = actionMgr.getFilesAccessed(parentActionId, OperationType.OP_WRITE); assertEquals(2, fileWrites.length); int flag1Id = fileMgr.getPath("/tmp/flag-file1"); int flag2Id = fileMgr.getPath("/tmp/flag-file2"); assertNotSame(ErrorCode.BAD_PATH, flag1Id); assertNotSame(ErrorCode.BAD_PATH, flag2Id); assertTrue(CommonTestUtils.sortedArraysEqual(new Integer[] {flag1Id, flag2Id}, fileWrites)); /* And the child action wrote to /tmp/flag-file3 */ fileWrites = actionMgr.getFilesAccessed(childActionId, OperationType.OP_WRITE); assertEquals(1, fileWrites.length); int flag3Id = fileMgr.getPath("/tmp/flag-file3"); assertNotSame(ErrorCode.BAD_PATH, flag3Id); assertTrue(CommonTestUtils.sortedArraysEqual(new Integer[] {flag3Id}, fileWrites)); /* * Validate that the actions executed in the expected directory. */ int currentDirId = fileMgr.getPath(PathUtils.normalizeAbsolutePath(new File(".").getAbsolutePath())); int parentDirId = (Integer) actionMgr.getSlotValue(parentActionId, IActionMgr.DIRECTORY_SLOT_ID); assertEquals(currentDirId, parentDirId); int childDirId = (Integer) actionMgr.getSlotValue(childActionId, IActionMgr.DIRECTORY_SLOT_ID); assertEquals(currentDirId, childDirId); /* * Validate that the child command's arguments are correct. That is, the absolute * path name to the child, followed by the expected arguments. */ String cmdLine = (String) actionMgr.getSlotValue(childActionId, IActionMgr.COMMAND_SLOT_ID); assertEquals(childPath + " arg1 arg2 arg3", cmdLine); } /*-------------------------------------------------------------------------------------*/ /** * A helper method for tracing the behaviour of system()-like functions. This is * similar to traceExecProgram(), but a lot less plumbing is required. * @param extraLines The command to be passed into the system() call. * @throws Exception */ private void traceSystemProgram(String extraLines) throws Exception { /* the source for the program is very simple, and we ignore return code */ String source = "int main() {" + " system(\"" + extraLines + "\");" + " return 0;" + "}"; /* trace the program's behaviour into a BuildStore */ bs = BuildScannersCommonTestUtils.parseLegacyProgram(tmpDir, source, null); fileMgr = bs.getFileMgr(); actionMgr = bs.getActionMgr(); /* validate the top-level action (the process that invokes "system"). */ Integer [] actions = actionMgr.getChildren(actionMgr.getRootAction("root")); assertEquals(1, actions.length); int systemActionId = actions[0].intValue(); /* the system action has a single child (the shell action). */ actions = actionMgr.getChildren(systemActionId); assertEquals(1, actions.length); shellActionId = actions[0].intValue(); } /*=====================================================================================* * Test cases for C-library functions. *=====================================================================================*/ /** * Test the execl() C function. * @throws Exception */ @Test public void testExecl() throws Exception { traceExecProgram( "execl(\"INSERT_PATH\", \"child\", \"arg1\", \"arg2\", \"arg3\", 0);", true); } /*-------------------------------------------------------------------------------------*/ /** * Test the execl() C function with the LD_PRELOAD environment variable removed. It * should be reintroduced before the child process is invoked. * @throws Exception */ @Test public void testExeclRemovePreload() throws Exception { traceExecProgram( "unsetenv(\"LD_PRELOAD\");" + "execl(\"INSERT_PATH\", \"child\", \"arg1\", \"arg2\", \"arg3\", 0);", true); } /*-------------------------------------------------------------------------------------*/ /** * Test the execl() C function with the LD_PRELOAD environment variable already set to * some other value. A warning message should be provided. * @throws Exception */ @Test public void testExeclOverridePreload() throws Exception { traceExecProgram( "setenv(\"LD_PRELOAD\", \"/usr/lib/libc.so\", 1);" + "execl(\"INSERT_PATH\", \"child\", \"arg1\", \"arg2\", \"arg3\", 0);", true); } /*-------------------------------------------------------------------------------------*/ /** * Test the execl() C function with the CFS_ID environment variable removed. It should * be reintroduced before the child process is invoked. * @throws Exception */ @Test public void testExeclRemoveCfsId() throws Exception { traceExecProgram( "unsetenv(\"CFS_ID\");" + "execl(\"INSERT_PATH\", \"child\", \"arg1\", \"arg2\", \"arg3\", 0);", true); } /*-------------------------------------------------------------------------------------*/ /** * Test the execl() C function with the CFS_PARENT_ID environment variable removed. * It should be reintroduced before the child process is invoked. * @throws Exception */ @Test public void testExeclRemoveCfsParentId() throws Exception { traceExecProgram( "unsetenv(\"CFS_PARENT_ID\");" + "execl(\"INSERT_PATH\", \"child\", \"arg1\", \"arg2\", \"arg3\", 0);", true); } /*-------------------------------------------------------------------------------------*/ /** * Test the execle() C function. * @throws Exception */ @Test public void testExecle() throws Exception { traceExecProgram( "execle(\"INSERT_PATH\", \"child\", \"arg1\", \"arg2\", \"arg3\", 0, environ);", true); } /*-------------------------------------------------------------------------------------*/ /** * Test the execle() C function, with a couple of empty strings as arguments. Note that * we don't care about the return value, but if empty arguments aren't accepted, this * code will crash. * @throws Exception */ @Test public void testExecleWithNullArgs() throws Exception { String source = "#include <unistd.h>\n" + "extern char **environ;" + "int main() {" + " execle(\"true\", \"true\", \"arg1\", \"\", \"arg3\", \"\", 0, environ);" + " return 0;" + "}"; bs = BuildScannersCommonTestUtils.parseLegacyProgram(tmpDir, source, null); } /*-------------------------------------------------------------------------------------*/ /** * Test the execlp() C function. * @throws Exception */ @Test public void testExeclp() throws Exception { traceExecProgram( "execlp(\"INSERT_PATH\", \"child\", \"arg1\", \"arg2\", \"arg3\", 0);", true); } /*-------------------------------------------------------------------------------------*/ /** * Test the execv() C function. * @throws Exception */ @Test public void testExecv() throws Exception { traceExecProgram("execv(\"INSERT_PATH\", tmp_argv);", true); } /*-------------------------------------------------------------------------------------*/ /** * Test the execve() C function. * @throws Exception */ @Test public void testExecve() throws Exception { traceExecProgram("execve(\"INSERT_PATH\", tmp_argv, environ);", true); } /*-------------------------------------------------------------------------------------*/ /** * Test the execvp() C function. * @throws Exception */ @Test public void testExecvp() throws Exception { traceExecProgram("execvp(\"INSERT_PATH\", tmp_argv);", true); } /*-------------------------------------------------------------------------------------*/ /** * Test the execvpe() C function. * @throws Exception */ @Test public void testExecvpe() throws Exception { traceExecProgram("execvpe(\"INSERT_PATH\", tmp_argv, environ);", true); } /*-------------------------------------------------------------------------------------*/ /** * Test the fexecve() C function. * @throws Exception */ @Test public void testFexecve() throws Exception { traceExecProgram( "{ " + " int fd = open(\"INSERT_PATH\", O_RDONLY);" + " fexecve(fd, tmp_argv, environ);" + "}", true); } /*-------------------------------------------------------------------------------------*/ /** * Common code for testing the fork() and vfork() functions. * @param func The function to test ("fork" or "vfork"). * @throws Exception */ public void testForkCmn(String func) throws Exception { String source = "int main() {" + " if (" + func + "() == 0) {" + " creat(\"/tmp/flag-file1\", 0666);" + " } else {" + " creat(\"/tmp/flag-file2\", 0666);" + " int status;" + " wait(&status);" + " }" + " return 0;" + "}"; bs = BuildScannersCommonTestUtils.parseLegacyProgram(tmpDir, source, null); fileMgr = bs.getFileMgr(); actionMgr = bs.getActionMgr(); /* validate the top-level action (the process that invokes fork()). */ Integer [] actions = actionMgr.getChildren(actionMgr.getRootAction("root")); assertEquals(1, actions.length); int forkActionId = actions[0].intValue(); /* check that this action accessed both flag-file1 and flag-file2 */ int file1Id = fileMgr.getPath("/tmp/flag-file1"); int file2Id = fileMgr.getPath("/tmp/flag-file2"); Integer fileWrites[] = actionMgr.getFilesAccessed(forkActionId, OperationType.OP_WRITE); assertTrue(CommonTestUtils.sortedArraysEqual(fileWrites, new Integer[] {file1Id, file2Id})); } /*-------------------------------------------------------------------------------------*/ /** * Test the fork() C function. * @throws Exception */ @Test public void testFork() throws Exception { testForkCmn("fork"); } /*-------------------------------------------------------------------------------------*/ /** * Test the popen() C function. * @throws Exception */ @Test public void testPopen() throws Exception { String source = "#include <stdio.h>\n" + "int main() {" + " FILE *f = popen(\"echo Hello World\", \"r\");" + " fclose(f);" + " return 0;" + "}"; bs = BuildScannersCommonTestUtils.parseLegacyProgram(tmpDir, source, null); fileMgr = bs.getFileMgr(); actionMgr = bs.getActionMgr(); /* validate the top-level action (the process that invokes "popen"). */ Integer [] actions = actionMgr.getChildren(actionMgr.getRootAction("root")); assertEquals(1, actions.length); int popenActionId = actions[0].intValue(); /* the popen action has a single child (the shell action). */ actions = actionMgr.getChildren(popenActionId); assertEquals(1, actions.length); shellActionId = actions[0].intValue(); /* the shell action has no child ("echo" is built-in to the shell */ actions = actionMgr.getChildren(shellActionId); assertEquals(0, actions.length); } /*-------------------------------------------------------------------------------------*/ /** * Test the posix_spawn() C function. * @throws Exception */ @Test public void testPosix_spawn() throws Exception { traceExecProgram( "{ " + " pid_t pid;" + " int status;" + " posix_spawn(&pid, \"INSERT_PATH\", NULL, NULL, tmp_argv, environ);" + " wait(&status);" + " if (WEXITSTATUS(status) == 123) { creat(\"/tmp/flag-file2\", 0666); }" + "}", false); } /*-------------------------------------------------------------------------------------*/ /** * Test the posix_spawnp() C function. * @throws Exception */ @Test public void testPosix_spawnp() throws Exception { traceExecProgram( "{ " + " pid_t pid;" + " int status;" + " posix_spawnp(&pid, \"INSERT_PATH\", NULL, NULL, tmp_argv, environ);" + " wait(&status);" + " if (WEXITSTATUS(status) == 123) { creat(\"/tmp/flag-file2\", 0666); }" + "}", false); } /*-------------------------------------------------------------------------------------*/ /** * Test the system() C function with a single shell command (without redirection or pipes). * This creates a top-level action which calls system() to create a shell action, which exec()'s * itself (without forking) to run the underlying command action. That gives three actions in total. * @throws Exception */ @Test public void testSystem() throws Exception { /* The command to be tested... */ traceSystemProgram("cp /etc/passwd /tmp/flag-file4"); /* the shell action has a single child (the underlying command). */ Integer actions[] = actionMgr.getChildren(shellActionId); assertEquals(1, actions.length); int cmdActionId = actions[0].intValue(); /* the command action has no children */ actions = actionMgr.getChildren(cmdActionId); assertEquals(0, actions.length); /* check that /etc/passwd and /tmp/flag-file4 have been accessed. */ assertNotSame(ErrorCode.BAD_PATH, fileMgr.getPath("/etc/passwd")); assertNotSame(ErrorCode.BAD_PATH, fileMgr.getPath("/tmp/flag-file4")); } /*-------------------------------------------------------------------------------------*/ /** * Test the system() C function, with standard output being redirected to a file. * @throws Exception */ @Test public void testSystemWithRedirect() throws Exception { /* The command to be tested... */ traceSystemProgram("cat /etc/passwd >/tmp/flag-file4"); /* the shell action has a single child (the underlying command). */ Integer actions[] = actionMgr.getChildren(shellActionId); assertEquals(1, actions.length); int cmdActionId = actions[0].intValue(); /* the command action has no children */ actions = actionMgr.getChildren(cmdActionId); assertEquals(0, actions.length); /* check that /etc/passwd and /tmp/flag-file4 have been accessed. */ assertNotSame(ErrorCode.BAD_PATH, fileMgr.getPath("/etc/passwd")); assertNotSame(ErrorCode.BAD_PATH, fileMgr.getPath("/tmp/flag-file4")); } /*-------------------------------------------------------------------------------------*/ /** * Test the system() C function, with two commands connected by a pipe. * @throws Exception */ @Test public void testSystemWithPipe() throws Exception { /* The command to be tested... */ traceSystemProgram("cat /etc/passwd | wc -l > /dev/null"); /* the shell action has two children (cat and wc). */ Integer actions[] = actionMgr.getChildren(shellActionId); assertEquals(2, actions.length); /* neither of these actions have children */ assertEquals(0, actionMgr.getChildren(actions[0].intValue()).length); assertEquals(0, actionMgr.getChildren(actions[1].intValue()).length); /* check that /etc/passwd has been accessed. */ assertNotSame(ErrorCode.BAD_PATH, fileMgr.getPath("/etc/passwd")); } /*-------------------------------------------------------------------------------------*/ /** * Test the system() C function, with two commands connected with && * @throws Exception */ @Test public void testSystemWithAndAnd() throws Exception { /* The command to be tested... */ traceSystemProgram("cat /etc/passwd > /dev/null && cat /etc/group >/dev/null"); /* the shell action has two children (cat and cat). */ Integer actions[] = actionMgr.getChildren(shellActionId); assertEquals(2, actions.length); /* neither of these actions have children */ assertEquals(0, actionMgr.getChildren(actions[0].intValue()).length); assertEquals(0, actionMgr.getChildren(actions[1].intValue()).length); } /*-------------------------------------------------------------------------------------*/ /** * Test the vfork() C function. * @throws Exception */ @Test public void testVfork() throws Exception { testForkCmn("vfork"); } /*-------------------------------------------------------------------------------------*/ /** * Test an exec call that fails. No child process should be registered. * @throws Exception */ @Test public void testExecFailure() throws Exception { String source = "int main() {" + " execl(\"/bad-path\", \"bad-path\", 0);" + " return 0;" + "}"; IBuildStore bs = BuildScannersCommonTestUtils.parseLegacyProgram(tmpDir, source, null); IActionMgr actionMgr = bs.getActionMgr(); /* there should be one top-level action (the one we just ran). */ int rootAction = actionMgr.getRootAction("root"); Integer children[] = actionMgr.getChildren(rootAction); assertEquals(1, children.length); /* there should be no second level actions */ int parentAction = children[0].intValue(); children = actionMgr.getChildren(parentAction); assertEquals(0, children.length); } /*-------------------------------------------------------------------------------------*/ }