diff --git a/os/linux/Makefile b/os/linux/Makefile index 9871f0ac5..c8e3c7e2c 100644 --- a/os/linux/Makefile +++ b/os/linux/Makefile @@ -106,28 +106,29 @@ libtest: $(LIBRARY_C_FILES) $(LIBRARY_TEST_C_FILES) $(YAML_AR) $(JSON_AR) $(TEST @echo "$${CI:+::group::}Building Library Tests" $(CC) -c $(TEST_CFLAGS) $(LIBRARY_C_FILES) $(LIBRARY_INCLUDES) $(LIBRARY_TEST_C_FILES) $(INCLUDES) $(CMOCKA_INCLUDES) $(OS_C_FILES) $(CC) $(TEST_CFLAGS) -o test/$(OS)/vdsotest vdsotest.o scopestdlib.o dbg.o test.o $(TEST_AR) $(TEST_LD_FLAGS) - $(CC) $(TEST_CFLAGS) -o test/$(OS)/coredumptest coredumptest.o coredump.o scopestdlib.o dbg.o utils.o fn.o plattime.o os.o test.o $(TEST_AR) $(TEST_LD_FLAGS) - $(CC) $(TEST_CFLAGS) -o test/$(OS)/ipctest ipctest.o ipc.o ipc_resp.o cfgutils.o cfg.o mtc.o log.o evtformat.o ctl.o transport.o backoff.o mtcformat.o strset.o com.o scopestdlib.o dbg.o circbuf.o linklist.o fn.o utils.o os.o test.o report.o search.o httpagg.o state.o httpstate.o metriccapture.o plattime.o $(TEST_AR) $(TEST_LD_FLAGS) -Wl,--wrap=jsonConfigurationObject -Wl,--wrap=doAndReplaceConfig - $(CC) $(TEST_CFLAGS) -o test/$(OS)/snapshottest snapshottest.o snapshot.o coredump.o scopestdlib.o dbg.o utils.o fn.o plattime.o os.o test.o $(TEST_AR) $(TEST_LD_FLAGS) - $(CC) $(TEST_CFLAGS) -o test/$(OS)/ostest ostest.o scopestdlib.o dbg.o fn.o utils.o plattime.o os.o test.o $(TEST_AR) $(TEST_LD_FLAGS) + $(CC) $(TEST_CFLAGS) -o test/$(OS)/coredumptest coredumptest.o coredump.o scopestdlib.o dbg.o utils.o fn.o plattime.o os.o scopeelf.o test.o $(TEST_AR) $(TEST_LD_FLAGS) + $(CC) $(TEST_CFLAGS) -o test/$(OS)/ipctest ipctest.o ipc.o ipc_resp.o cfgutils.o cfg.o mtc.o log.o evtformat.o ctl.o transport.o backoff.o mtcformat.o strset.o com.o scopestdlib.o dbg.o circbuf.o linklist.o fn.o utils.o os.o test.o report.o search.o httpagg.o state.o httpstate.o metriccapture.o plattime.o scopeelf.o $(TEST_AR) $(TEST_LD_FLAGS) -Wl,--wrap=jsonConfigurationObject -Wl,--wrap=doAndReplaceConfig + $(CC) $(TEST_CFLAGS) -o test/$(OS)/snapshottest snapshottest.o snapshot.o coredump.o scopestdlib.o dbg.o utils.o fn.o plattime.o os.o scopeelf.o test.o $(TEST_AR) $(TEST_LD_FLAGS) + $(CC) $(TEST_CFLAGS) -o test/$(OS)/ostest ostest.o scopestdlib.o dbg.o fn.o utils.o plattime.o os.o scopeelf.o test.o $(TEST_AR) $(TEST_LD_FLAGS) + $(CC) $(TEST_CFLAGS) -o test/$(OS)/ocitest ocitest.o scopestdlib.o dbg.o test.o $(TEST_AR) $(TEST_LD_FLAGS) $(CC) $(TEST_CFLAGS) -o test/$(OS)/strsettest strsettest.o strset.o scopestdlib.o dbg.o test.o $(TEST_AR) $(TEST_LD_FLAGS) - $(CC) $(TEST_CFLAGS) -o test/$(OS)/cfgutilstest cfgutilstest.o cfgutils.o cfg.o mtc.o log.o evtformat.o ctl.o transport.o backoff.o mtcformat.o strset.o com.o scopestdlib.o dbg.o circbuf.o linklist.o fn.o utils.o os.o test.o report.o search.o httpagg.o state.o httpstate.o metriccapture.o plattime.o $(TEST_AR) $(TEST_LD_FLAGS) + $(CC) $(TEST_CFLAGS) -o test/$(OS)/cfgutilstest cfgutilstest.o cfgutils.o cfg.o mtc.o log.o evtformat.o ctl.o transport.o backoff.o mtcformat.o strset.o com.o scopestdlib.o dbg.o circbuf.o linklist.o fn.o utils.o os.o test.o report.o search.o httpagg.o state.o httpstate.o metriccapture.o plattime.o scopeelf.o $(TEST_AR) $(TEST_LD_FLAGS) $(CC) $(TEST_CFLAGS) -o test/$(OS)/cfgtest cfgtest.o cfg.o scopestdlib.o dbg.o test.o $(TEST_AR) $(TEST_LD_FLAGS) - $(CC) $(TEST_CFLAGS) -o test/$(OS)/transporttest transporttest.o transport.o backoff.o scopestdlib.o dbg.o log.o fn.o utils.o plattime.o os.o test.o $(TEST_AR) $(TEST_LD_FLAGS) + $(CC) $(TEST_CFLAGS) -o test/$(OS)/transporttest transporttest.o transport.o backoff.o scopestdlib.o dbg.o log.o fn.o utils.o plattime.o os.o scopeelf.o test.o $(TEST_AR) $(TEST_LD_FLAGS) $(CC) $(TEST_CFLAGS) -o test/$(OS)/backofftest backofftest.o backoff.o scopestdlib.o dbg.o test.o $(TEST_AR) $(TEST_LD_FLAGS) - $(CC) $(TEST_CFLAGS) -o test/$(OS)/logtest logtest.o log.o transport.o backoff.o scopestdlib.o dbg.o fn.o utils.o plattime.o os.o test.o $(TEST_AR) $(TEST_LD_FLAGS) - $(CC) $(TEST_CFLAGS) -o test/$(OS)/utilstest utilstest.o scopestdlib.o dbg.o fn.o utils.o plattime.o os.o test.o $(TEST_AR) $(TEST_LD_FLAGS) - $(CC) $(TEST_CFLAGS) -o test/$(OS)/mtctest mtctest.o mtc.o log.o transport.o backoff.o mtcformat.o strset.o com.o ctl.o evtformat.o cfg.o cfgutils.o scopestdlib.o dbg.o circbuf.o linklist.o fn.o utils.o os.o test.o report.o search.o httpagg.o state.o httpstate.o metriccapture.o plattime.o $(TEST_AR) $(TEST_LD_FLAGS) -Wl,--wrap=cfgLogStreamEnable - $(CC) $(TEST_CFLAGS) -o test/$(OS)/evtformattest evtformattest.o evtformat.o log.o transport.o backoff.o mtcformat.o strset.o scopestdlib.o dbg.o cfg.o com.o ctl.o mtc.o circbuf.o cfgutils.o linklist.o fn.o utils.o os.o test.o report.o search.o httpagg.o state.o httpstate.o metriccapture.o plattime.o $(TEST_AR) $(TEST_LD_FLAGS) - $(CC) $(TEST_CFLAGS) -o test/$(OS)/ctltest ctltest.o ctl.o log.o transport.o backoff.o scopestdlib.o dbg.o cfgutils.o cfg.o com.o mtc.o evtformat.o mtcformat.o strset.o circbuf.o linklist.o fn.o utils.o os.o test.o report.o search.o httpagg.o state.o httpstate.o metriccapture.o plattime.o $(TEST_AR) $(TEST_LD_FLAGS) -Wl,--wrap=cbufGet - $(CC) $(TEST_CFLAGS) -o test/$(OS)/httpstatetest httpstatetest.o httpstate.o plattime.o search.o fn.o utils.o os.o scopestdlib.o dbg.o test.o com.o cfg.o cfgutils.o mtc.o mtcformat.o strset.o ctl.o transport.o backoff.o linklist.o log.o evtformat.o circbuf.o state.o metriccapture.o report.o httpagg.o $(TEST_AR) $(TEST_LD_FLAGS) -Wl,--wrap=cmdPostEvent -lrt - $(CC) $(TEST_CFLAGS) -o test/$(OS)/httpheadertest httpheadertest.o report.o httpagg.o state.o com.o httpstate.o metriccapture.o plattime.o fn.o utils.o os.o ctl.o log.o transport.o backoff.o scopestdlib.o dbg.o cfgutils.o cfg.o mtc.o evtformat.o mtcformat.o strset.o circbuf.o linklist.o search.o test.o $(TEST_AR) $(TEST_LD_FLAGS) -Wl,--wrap=cmdSendHttp -Wl,--wrap=cmdPostEvent - $(CC) $(TEST_CFLAGS) -o test/$(OS)/httpaggtest httpaggtest.o httpagg.o fn.o utils.o scopestdlib.o dbg.o plattime.o os.o test.o $(TEST_AR) $(TEST_LD_FLAGS) - $(CC) $(TEST_CFLAGS) -o test/$(OS)/reporttest reporttest.o report.o httpagg.o state.o httpstate.o metriccapture.o com.o plattime.o fn.o utils.o os.o ctl.o log.o transport.o backoff.o scopestdlib.o dbg.o cfgutils.o cfg.o mtc.o evtformat.o mtcformat.o strset.o circbuf.o linklist.o search.o test.o $(TEST_AR) $(TEST_LD_FLAGS) -Wl,--wrap=cmdSendEvent -Wl,--wrap=cmdSendMetric - $(CC) $(TEST_CFLAGS) -o test/$(OS)/mtcformattest mtcformattest.o mtcformat.o strset.o scopestdlib.o dbg.o log.o transport.o backoff.o com.o ctl.o mtc.o evtformat.o cfg.o cfgutils.o linklist.o fn.o utils.o circbuf.o os.o test.o report.o search.o httpagg.o state.o httpstate.o metriccapture.o plattime.o $(TEST_AR) $(TEST_LD_FLAGS) + $(CC) $(TEST_CFLAGS) -o test/$(OS)/logtest logtest.o log.o transport.o backoff.o scopestdlib.o dbg.o fn.o utils.o plattime.o os.o scopeelf.o test.o $(TEST_AR) $(TEST_LD_FLAGS) + $(CC) $(TEST_CFLAGS) -o test/$(OS)/utilstest utilstest.o scopestdlib.o dbg.o fn.o utils.o plattime.o os.o scopeelf.o test.o $(TEST_AR) $(TEST_LD_FLAGS) + $(CC) $(TEST_CFLAGS) -o test/$(OS)/mtctest mtctest.o mtc.o log.o transport.o backoff.o mtcformat.o strset.o com.o ctl.o evtformat.o cfg.o cfgutils.o scopestdlib.o dbg.o circbuf.o linklist.o fn.o utils.o os.o test.o report.o search.o httpagg.o state.o httpstate.o metriccapture.o plattime.o scopeelf.o $(TEST_AR) $(TEST_LD_FLAGS) -Wl,--wrap=cfgLogStreamEnable + $(CC) $(TEST_CFLAGS) -o test/$(OS)/evtformattest evtformattest.o evtformat.o log.o transport.o backoff.o mtcformat.o strset.o scopestdlib.o dbg.o cfg.o com.o ctl.o mtc.o circbuf.o cfgutils.o linklist.o fn.o utils.o os.o test.o report.o search.o httpagg.o state.o httpstate.o metriccapture.o plattime.o scopeelf.o $(TEST_AR) $(TEST_LD_FLAGS) + $(CC) $(TEST_CFLAGS) -o test/$(OS)/ctltest ctltest.o ctl.o log.o transport.o backoff.o scopestdlib.o dbg.o cfgutils.o cfg.o com.o mtc.o evtformat.o mtcformat.o strset.o circbuf.o linklist.o fn.o utils.o os.o test.o report.o search.o httpagg.o state.o httpstate.o metriccapture.o plattime.o scopeelf.o $(TEST_AR) $(TEST_LD_FLAGS) -Wl,--wrap=cbufGet + $(CC) $(TEST_CFLAGS) -o test/$(OS)/httpstatetest httpstatetest.o httpstate.o plattime.o search.o fn.o utils.o os.o scopestdlib.o dbg.o test.o com.o cfg.o cfgutils.o mtc.o mtcformat.o strset.o ctl.o transport.o backoff.o linklist.o log.o evtformat.o circbuf.o state.o metriccapture.o report.o httpagg.o scopeelf.o $(TEST_AR) $(TEST_LD_FLAGS) -Wl,--wrap=cmdPostEvent -lrt + $(CC) $(TEST_CFLAGS) -o test/$(OS)/httpheadertest httpheadertest.o report.o httpagg.o state.o com.o httpstate.o metriccapture.o plattime.o fn.o utils.o os.o ctl.o log.o transport.o backoff.o scopestdlib.o dbg.o cfgutils.o cfg.o mtc.o evtformat.o mtcformat.o strset.o circbuf.o linklist.o search.o scopeelf.o test.o $(TEST_AR) $(TEST_LD_FLAGS) -Wl,--wrap=cmdSendHttp -Wl,--wrap=cmdPostEvent + $(CC) $(TEST_CFLAGS) -o test/$(OS)/httpaggtest httpaggtest.o httpagg.o fn.o utils.o scopestdlib.o dbg.o plattime.o os.o scopeelf.o test.o $(TEST_AR) $(TEST_LD_FLAGS) + $(CC) $(TEST_CFLAGS) -o test/$(OS)/reporttest reporttest.o report.o httpagg.o state.o httpstate.o metriccapture.o com.o plattime.o fn.o utils.o os.o ctl.o log.o transport.o backoff.o scopestdlib.o dbg.o cfgutils.o cfg.o mtc.o evtformat.o mtcformat.o strset.o circbuf.o linklist.o search.o scopeelf.o test.o $(TEST_AR) $(TEST_LD_FLAGS) -Wl,--wrap=cmdSendEvent -Wl,--wrap=cmdSendMetric + $(CC) $(TEST_CFLAGS) -o test/$(OS)/mtcformattest mtcformattest.o mtcformat.o strset.o scopestdlib.o dbg.o log.o transport.o backoff.o com.o ctl.o mtc.o evtformat.o cfg.o cfgutils.o linklist.o fn.o utils.o circbuf.o os.o test.o report.o search.o httpagg.o state.o httpstate.o metriccapture.o plattime.o scopeelf.o $(TEST_AR) $(TEST_LD_FLAGS) $(CC) $(TEST_CFLAGS) -o test/$(OS)/circbuftest circbuftest.o circbuf.o scopestdlib.o dbg.o test.o $(TEST_AR) $(TEST_LD_FLAGS) $(CC) $(TEST_CFLAGS) -o test/$(OS)/linklisttest linklisttest.o linklist.o scopestdlib.o dbg.o test.o $(TEST_AR) $(TEST_LD_FLAGS) - $(CC) $(TEST_CFLAGS) -o test/$(OS)/comtest comtest.o com.o ctl.o log.o transport.o backoff.o evtformat.o circbuf.o mtcformat.o strset.o cfgutils.o cfg.o mtc.o scopestdlib.o dbg.o linklist.o fn.o utils.o os.o test.o report.o search.o httpagg.o state.o httpstate.o metriccapture.o plattime.o $(TEST_AR) $(TEST_LD_FLAGS) + $(CC) $(TEST_CFLAGS) -o test/$(OS)/comtest comtest.o com.o ctl.o log.o transport.o backoff.o evtformat.o circbuf.o mtcformat.o strset.o cfgutils.o cfg.o mtc.o scopestdlib.o dbg.o linklist.o fn.o utils.o os.o test.o report.o search.o httpagg.o state.o httpstate.o metriccapture.o plattime.o scopeelf.o $(TEST_AR) $(TEST_LD_FLAGS) $(CC) $(TEST_CFLAGS) -o test/$(OS)/dbgtest dbgtest.o scopestdlib.o dbg.o test.o $(TEST_AR) $(TEST_LD_FLAGS) $(CC) $(TEST_CFLAGS) -o test/$(OS)/glibcvertest glibcvertest.o $(TEST_AR) $(TEST_LD_FLAGS) $(CC) $(TEST_CFLAGS) -o test/$(OS)/selfinterposetest selfinterposetest.o $(TEST_AR) $(TEST_LD_FLAGS) diff --git a/os/linux/os.c b/os/linux/os.c index 60ec98e9f..f817a447b 100644 --- a/os/linux/os.c +++ b/os/linux/os.c @@ -555,17 +555,50 @@ osGetCmdline(pid_t pid, char **cmd) buf = scope_strdup("none"); } else { // buf is big; try to scope_strdup what we've used and scope_free the rest - char* tmp = scope_strdup(buf); + char *tmp = scope_strdup(buf); if (tmp) { scope_free(buf); buf = tmp; } } + if (fd != -1) scope_close(fd); *cmd = buf; return (*cmd != NULL); } +int +osGetArgv(pid_t pid, char *buf, size_t blen) +{ + int i, fd = -1, argc = 0, bytesRead = 0; + char path[64]; + + if (!buf) return 0; + + if (scope_snprintf(path, sizeof(path), "/proc/%d/cmdline", pid) < 0) { + goto out; + } + + if ((fd = scope_open(path, O_RDONLY)) == -1) { + DBG(NULL); + goto out; + } + + if ((bytesRead = scope_read(fd, buf, blen)) <= 0) { + DBG(NULL); + goto out; + } + + // Replace all but the last null with spaces + for (i=0; i < (bytesRead - 1); i++) { + if (buf[i] == '\0') argc++; + } + +out: + if (fd != -1) scope_close(fd); + return argc; +} + bool osTimerStop(void) { diff --git a/os/linux/os.h b/os/linux/os.h index 669c2b256..5ba2e4f28 100644 --- a/os/linux/os.h +++ b/os/linux/os.h @@ -71,5 +71,6 @@ extern void osCreateSM(proc_id_t *, unsigned long); extern bool osMemPermAllow(void *, size_t, int, int); extern bool osMemPermRestore(void *, size_t, int); extern bool osGetBaseAddr(uint64_t *); +extern int osGetArgv(pid_t, char *, size_t); #endif //__OS_H__ diff --git a/src/dbg.c b/src/dbg.c index 3f5b9c22d..d11a88a5e 100644 --- a/src/dbg.c +++ b/src/dbg.c @@ -39,6 +39,7 @@ bool g_ismusl = FALSE; bool g_isstatic = FALSE; bool g_isgo = FALSE; bool g_issighandler = FALSE; +char g_libpath[PATH_MAX] = {}; void dbgInit() diff --git a/src/dbg.h b/src/dbg.h index a8ecac027..53985a5e2 100644 --- a/src/dbg.h +++ b/src/dbg.h @@ -79,6 +79,7 @@ extern bool g_ismusl; extern bool g_isstatic; extern bool g_isgo; extern bool g_issighandler; +extern char g_libpath[]; void scopeLog(cfg_log_level_t, const char *, ...) PRINTF_FORMAT(2,3); void scopeLogHex(cfg_log_level_t, const void *, size_t, const char *, ...) PRINTF_FORMAT(4,5); diff --git a/src/scopeelf.c b/src/scopeelf.c index d339e4d27..9b2c60b7d 100644 --- a/src/scopeelf.c +++ b/src/scopeelf.c @@ -301,6 +301,56 @@ getElfEntries(struct link_map *lm, Elf64_Rela **rel, Elf64_Sym **sym, char **str return 0; } +void * +getDynSymbol(const char *buf, char *sname) +{ + int i, nsyms = 0; + Elf64_Addr symaddr = 0; + Elf64_Ehdr *ehdr; + Elf64_Shdr *sections; + Elf64_Sym *symtab = NULL; + const char *section_strtab = NULL; + const char *strtab = NULL; + const char *sec_name = NULL; + + if (!buf || !sname) return NULL; + + ehdr = (Elf64_Ehdr *)buf; + sections = (Elf64_Shdr *)((char *)buf + ehdr->e_shoff); + section_strtab = (char *)buf + sections[ehdr->e_shstrndx].sh_offset; + + for (i = 0; i < ehdr->e_shnum; i++) { + sec_name = section_strtab + sections[i].sh_name; + + if (sections[i].sh_type == SHT_DYNSYM) { + symtab = (Elf64_Sym *)((char *)buf + sections[i].sh_offset); + nsyms = sections[i].sh_size / sections[i].sh_entsize; + } else if (sections[i].sh_type == SHT_STRTAB && scope_strcmp(sec_name, ".dynstr") == 0) { + strtab = (const char *)(buf + sections[i].sh_offset); + } + + if ((strtab != NULL) && (symtab != NULL)) break; + scopeLogDebug("section %s type = %d flags = 0x%lx addr = 0x%lx-0x%lx, size = 0x%lx off = 0x%lx\n", + sec_name, + sections[i].sh_type, + sections[i].sh_flags, + sections[i].sh_addr, + sections[i].sh_addr + sections[i].sh_size, + sections[i].sh_size, + sections[i].sh_offset); + } + + for (i=0; i < nsyms; i++) { + if (scope_strcmp(sname, strtab + symtab[i].st_name) == 0) { + symaddr = symtab[i].st_value; + sysprint("symbol found %s = 0x%08lx\n", strtab + symtab[i].st_name, symtab[i].st_value); + break; + } + } + + return (void *)symaddr; +} + void * getSymbol(const char *buf, char *sname) { diff --git a/src/scopeelf.h b/src/scopeelf.h index 947f934e8..04b790b40 100644 --- a/src/scopeelf.h +++ b/src/scopeelf.h @@ -26,6 +26,7 @@ int doGotcha(struct link_map *, got_list_t *, Elf64_Rela *, Elf64_Sym *, char *, int getElfEntries(struct link_map *, Elf64_Rela **, Elf64_Sym **, char **, int *rsz); Elf64_Shdr* getElfSection(char *, const char *); void * getSymbol(const char *, char *); +void * getDynSymbol(const char *, char *); bool is_static(char *); bool is_go(char *); bool is_musl(char *); diff --git a/src/scopetypes.h b/src/scopetypes.h index ffa9dfefd..b8c235d44 100644 --- a/src/scopetypes.h +++ b/src/scopetypes.h @@ -51,6 +51,8 @@ typedef enum {CFG_MTC_FS, #define SM_NAME "scope_anon" #define SCOPE_FILTER_USR_PATH ("/usr/lib/appscope/scope_filter") #define SCOPE_FILTER_TMP_PATH ("/tmp/appscope/scope_filter") +#define SCOPE_SYS_PATH "/usr/lib/appscope/" +#define SCOPE_TMP_PATH "/tmp/appscope/" typedef unsigned int bool; #define TRUE 1 diff --git a/src/sysexec.c b/src/sysexec.c index e47c0951a..a9fd3abbb 100644 --- a/src/sysexec.c +++ b/src/sysexec.c @@ -24,8 +24,6 @@ #include "state.h" #include "gocontext.h" -#define SYSPRINT_CONSOLE 0 -#define PRINT_BUF_SIZE 1024 #define HEAP_SIZE (size_t)(500 * 1024) // 1Mb + an 8kb guard #define STACK_SIZE (size_t)(1024 * 1024) + (8 * 1024) @@ -40,27 +38,6 @@ #define EXPORTON __attribute__((visibility("default"))) -void -sysprint(const char* fmt, ...) -{ - // Create the string - char str[PRINT_BUF_SIZE]; - - if (fmt) { - va_list args; - va_start(args, fmt); - int rv = scope_vsnprintf(str, PRINT_BUF_SIZE, fmt, args); - va_end(args); - if (rv == -1) return; - } - - // Output the string -#if SYSPRINT_CONSOLE > 0 - scope_printf("%s", str); -#endif - scopeLog(CFG_LOG_DEBUG, "%s", str); -} - static int get_file_size(const char *path) { diff --git a/src/transport.c b/src/transport.c index b8c4c4719..accd1294d 100644 --- a/src/transport.c +++ b/src/transport.c @@ -22,6 +22,7 @@ #include "fn.h" #include "utils.h" #include "transport.h" +#include "utils.h" // Yuck. Avoids naming conflict between our src/wrap.c and libssl.a #define SSL_read SCOPE_SSL_read @@ -1019,37 +1020,6 @@ transportConnectFile(transport_t *t) return 1; } -#define EDGE_PATH_DOCKER "/var/run/appscope/appscope.sock" -#define EDGE_PATH_DEFAULT "/opt/cribl/state/appscope.sock" -#define READ_AND_WRITE (R_OK|W_OK) -static char* -edgePath(void){ - // 1) If EDGE_PATH_DOCKER can be accessed, return that. - if (scope_access(EDGE_PATH_DOCKER, READ_AND_WRITE) == 0) { - return scope_strdup(EDGE_PATH_DOCKER); - } - - // 2) If CRIBL_HOME is defined and can be accessed, - // return $CRIBL_HOME/state/appscope.sock - const char *cribl_home = fullGetEnv("CRIBL_HOME"); - if (cribl_home) { - char *new_path = NULL; - if (scope_asprintf(&new_path, "%s/%s", cribl_home, "state/appscope.sock") > 0) { - if (scope_access(new_path, READ_AND_WRITE) == 0) { - return new_path; - } - scope_free(new_path); - } - } - - // 3) If EDGE_PATH_DEFAULT can be accessed, return it - if (scope_access(EDGE_PATH_DEFAULT, READ_AND_WRITE) == 0) { - return scope_strdup(EDGE_PATH_DEFAULT); - } - - return NULL; -} - int transportConnect(transport_t *trans) { diff --git a/src/utils.c b/src/utils.c index 8410d2651..3cb74efde 100644 --- a/src/utils.c +++ b/src/utils.c @@ -4,6 +4,7 @@ #include #include #include +#include #include "scopestdlib.h" #include "utils.h" @@ -11,7 +12,10 @@ #include "dbg.h" #include "runtimecfg.h" #include "plattime.h" +#include "scopeelf.h" +#define SYSPRINT_CONSOLE 0 +#define PRINT_BUF_SIZE 1024 #define MAC_ADDR_LEN 17 #define ZERO_MACHINE_ID "00000000000000000000000000000000" @@ -29,6 +33,27 @@ static int getMacAddr(char *string); rtconfig g_cfg = {0}; +void +sysprint(const char* fmt, ...) +{ + // Create the string + char str[PRINT_BUF_SIZE]; + + if (fmt) { + va_list args; + va_start(args, fmt); + int rv = scope_vsnprintf(str, PRINT_BUF_SIZE, fmt, args); + va_end(args); + if (rv == -1) return; + } + + // Output the string +#if SYSPRINT_CONSOLE > 0 + scope_printf("%s", str); +#endif + scopeLog(CFG_LOG_DEBUG, "%s", str); +} + unsigned int strToVal(enum_map_t map[], const char *str) { @@ -511,3 +536,67 @@ sigSafeWriteNumber(int fd, long val, int base) { return scope_write(fd, buf ,msgLen); } +/* + * Returns normalized version string + * - "dev" for unofficial release + * - "%d.%d.%d" for official release (e.g. "1.3.0" for "v1.3.0") + * - "%d.%d.%d-%s%d" for candidate release (e.g. "1.3.0-rc0" for "v1.3.0-rc0") + */ +const char * +libVersion(const char *version) { + + if ((version == NULL) || (*version != 'v')) { + return "dev"; + } + + ++version; + size_t versionSize = scope_strlen(version); + + for (int i = 0; i < versionSize; ++i) { + // Only digit and "." are accepted + if ((scope_isdigit(version[i]) == 0) && version[i] != '.' && + version[i] != '-' && version[i] != 't' && version[i] != 'c' && + version[i] != 'r') { + return "dev"; + } + if (i == 0 || i == versionSize) { + // First and last character must be number + if (scope_isdigit(version[i]) == 0) { + return "dev"; + } + } + } + return version; +} + +#define EDGE_PATH_DOCKER "/var/run/appscope/appscope.sock" +#define EDGE_PATH_DEFAULT "/opt/cribl/state/appscope.sock" +#define READ_AND_WRITE (R_OK|W_OK) + +char * +edgePath(void) { + // 1) If EDGE_PATH_DOCKER can be accessed, return that. + if (scope_access(EDGE_PATH_DOCKER, READ_AND_WRITE) == 0) { + return scope_strdup(EDGE_PATH_DOCKER); + } + + // 2) If CRIBL_HOME is defined and can be accessed, + // return $CRIBL_HOME/state/appscope.sock + const char *cribl_home = getenv("CRIBL_HOME"); + if (cribl_home) { + char *new_path = NULL; + if (scope_asprintf(&new_path, "%s/%s", cribl_home, "state/appscope.sock") > 0) { + if (scope_access(new_path, READ_AND_WRITE) == 0) { + return new_path; + } + scope_free(new_path); + } + } + + // 3) If EDGE_PATH_DEFAULT can be accessed, return it + if (scope_access(EDGE_PATH_DEFAULT, READ_AND_WRITE) == 0) { + return scope_strdup(EDGE_PATH_DEFAULT); + } + + return NULL; +} diff --git a/src/utils.h b/src/utils.h index c6189355a..2bddc69a5 100644 --- a/src/utils.h +++ b/src/utils.h @@ -24,6 +24,10 @@ int endsWith(const char *, const char *); void setUUID(char *); void setMachineID(char *); +char *edgePath(void); +const char *libVersion(const char *); +void sysprint(const char *, ...); + // Signal Safe API int sigSafeNanosleep(const struct timespec *); void sigSafeUtoa(unsigned long, char *, int, int *); diff --git a/src/wrap.c b/src/wrap.c index e961bd21c..36fc7ceba 100644 --- a/src/wrap.c +++ b/src/wrap.c @@ -1731,6 +1731,24 @@ init(void) bool scopedFlag = FALSE; bool skipReadCfg = FALSE; + /* + * This might be able to merge with the filter file check below. + * However, we dont want to check a specific file. We may update + * how we define the filter file. So, for now, leaving them + * distinct. + * + * Would be nice to use an env var without the need to access + * a file. However, we don't rely on something like SCOPE_LIB_PATH + * when LD_PRELOAD alone is used. + */ + if (!scope_access(SCOPE_SYS_PATH, R_OK)) { + scope_strncpy(g_libpath, SCOPE_SYS_PATH, sizeof(SCOPE_SYS_PATH)); + } else if (!scope_access(SCOPE_TMP_PATH, R_OK)) { + scope_strncpy(g_libpath, SCOPE_TMP_PATH, sizeof(SCOPE_TMP_PATH)); + } + + scope_strncat(g_libpath, libVersion(SCOPE_VER), sizeof(SCOPE_VER)); + if (attachedFlag) { scopedFlag = TRUE; } else { diff --git a/src/wrap_go.c b/src/wrap_go.c index 94bb47493..0f3e190cd 100644 --- a/src/wrap_go.c +++ b/src/wrap_go.c @@ -415,15 +415,24 @@ createGoStructFile(void) { } } -// Use go_str() whenever a "go string" type needs to be interpreted. -// The resulting go_str will need to be passed to free_go_str() when it is no -// longer needed. -// Don't use go_str() for byte arrays. +/* + * Use go_str() whenever a "go string" type needs to be interpreted. + * The resulting go_str will need to be passed to free_go_str() when it is no + * longer needed. + * Don't use go_str() for byte arrays. + * + * Go 17 and higher use "c style" null terminated strings instead of a string + * and a length. Therfore, we do nothing here for Go >= 17. + * However, there is a case where argv values are passed as go strings. + * In that case we we need to force a conversion even when we are >= Go 17. + * We are no longer interposing a function that references argv, however, the + * force param is left in place expecting that since it has been needed, it + * will be needed. + */ static char * -go_str(void *go_str) +go_str(void *go_str, bool force) { - // Go 17 and higher use "c style" null terminated strings instead of a string and a length - if (g_go_minor_ver >= 17) { + if ((g_go_minor_ver >= 17) && (force == FALSE)) { // We need to deference the address first before casting to a char * if (!go_str) return NULL; return (char *)*(uint64_t *)go_str; @@ -451,6 +460,356 @@ free_go_str(char *str) { } */ +/* + * Rewrite the container configuration for the given container. + * A path to the container specific work dir defines the location of the configuration file. + * + * Please look into opencontainers Linux runtime-spec for details about the exact JSON struct. + * The following changes will be performed: + * - Add a mount point + * `scope` will be mounted from the host ("ex: /usr/lib/appscope//scope") into the container ("ex: /opt/scope") + * - Extend Environment variables + * `LD_PRELOAD` will contain the following entry `/opt/libscope.so` + * `SCOPE_SETUP_DONE=true` mark that configuration was processed + * - Add prestart hook + * execute scope extract operation to ensure using library with proper loader reference (musl/glibc) + */ +static void +rewriteOpenContainersConfig(const char *cWorkDir) +{ + char path[PATH_MAX] = {0}; + if (!cWorkDir) { + goto exit; + } + + if (scope_snprintf(path, sizeof(path), "%s/config.json", cWorkDir) < 0) { + goto exit; + } + + struct stat fileStat; + if (scope_stat(path, &fileStat) == -1) { + goto exit; + } + + FILE *fp = scope_fopen(path, "r"); + if (!fp) { + goto exit; + } + + /* + * Read the file contents into a string + */ + char *buf = (char *)scope_malloc(fileStat.st_size); + if (!buf) { + scope_fclose(fp); + goto exit; + } + + scope_fread(buf, sizeof(char), fileStat.st_size, fp); + scope_fclose(fp); + + cJSON *json = cJSON_Parse(buf); + scope_free(buf); + if (json == NULL) { + goto exit; + } + + /* + * Handle process environment variables + * + "env":[ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "HOSTNAME=6735578591bb", + "TERM=xterm", + "LD_PRELOAD=/opt/libscope.so", + "SCOPE_SETUP_DONE=true" + ], + */ + cJSON *procNode = cJSON_GetObjectItemCaseSensitive(json, "process"); + if (!procNode) { + procNode = cJSON_CreateObject(); + if (!procNode) { + cJSON_Delete(json); + goto exit; + } + cJSON_AddItemToObject(json, "process", procNode); + } + + cJSON *envNodeArr = cJSON_GetObjectItemCaseSensitive(procNode, "env"); + if (envNodeArr) { + bool ldPreloadPresent = FALSE; + // Iterate over environment string array + size_t envSize = cJSON_GetArraySize(envNodeArr); + for (int i = 0; i < envSize ;++i) { + cJSON *item = cJSON_GetArrayItem(envNodeArr, i); + char *strItem = cJSON_GetStringValue(item); + + if (scope_strncmp("LD_PRELOAD=", strItem, sizeof("LD_PRELOAD=")-1) == 0) { + size_t itemLen = scope_strlen(strItem); + size_t newLdprelLen = itemLen + sizeof("/opt/libscope.so:") - 1; + char *newLdPreloadLib = scope_calloc(1, newLdprelLen); + if (!newLdPreloadLib) { + cJSON_Delete(json); + goto exit; + } + scope_strncpy(newLdPreloadLib, "LD_PRELOAD=/opt/libscope.so:", sizeof("LD_PRELOAD=/opt/libscope.so:") - 1); + scope_strcat(newLdPreloadLib, strItem + sizeof("LD_PRELOAD=") - 1); + cJSON *newLdPreloadLibObj = cJSON_CreateString(newLdPreloadLib); + if (!newLdPreloadLibObj) { + scope_free(newLdPreloadLib); + cJSON_Delete(json); + goto exit; + } + cJSON_ReplaceItemInArray(envNodeArr, i, newLdPreloadLibObj); + scope_free(newLdPreloadLib); + + cJSON *scopeEnvNode = cJSON_CreateString("SCOPE_SETUP_DONE=true"); + if (!scopeEnvNode) { + cJSON_Delete(json); + goto exit; + } + cJSON_AddItemToArray(envNodeArr, scopeEnvNode); + ldPreloadPresent = TRUE; + break; + } else if (scope_strncmp("SCOPE_SETUP_DONE=true", strItem, sizeof("SCOPE_SETUP_DONE=true")-1) == 0) { + // we are done here + cJSON_Delete(json); + goto exit; + } + } + + + // There was no LD_PRELOAD in environment variables + if (ldPreloadPresent == FALSE) { + const char *const envItems[2] = + { + "LD_PRELOAD=/opt/libscope.so", + "SCOPE_SETUP_DONE=true" + }; + for (int i = 0; i < 2 ;++i) { + cJSON *scopeEnvNode = cJSON_CreateString(envItems[i]); + if (!scopeEnvNode) { + cJSON_Delete(json); + goto exit; + } + cJSON_AddItemToArray(envNodeArr, scopeEnvNode); + } + } + } else { + const char * envItems[2] = + { + "LD_PRELOAD=/opt/libscope.so", + "SCOPE_SETUP_DONE=true" + }; + envNodeArr = cJSON_CreateStringArray(envItems, 2); + if (!envNodeArr) { + cJSON_Delete(json); + goto exit; + } + cJSON_AddItemToObject(procNode, "env", envNodeArr); + } + + /* + * Handle process mounts + * + "mounts":[ + { + "destination":"/proc", + "type":"proc", + "source":"proc", + "options":[ + "nosuid", + "noexec", + "nodev" + ] + }, + ... + { + "destination":"/opt/scope", + "type":"bind", + "source":"/tmp/appscope/dev/scope", + "options":[ + "rbind", + "rprivate" + ] + } + */ + cJSON *mountNodeArr = cJSON_GetObjectItemCaseSensitive(json, "mounts"); + if (!mountNodeArr) { + mountNodeArr = cJSON_CreateArray(); + if (!mountNodeArr) { + cJSON_Delete(json); + goto exit; + } + cJSON_AddItemToObject(json, "mounts", mountNodeArr); + } + + cJSON *mountNode = cJSON_CreateObject(); + if (!mountNode) { + cJSON_Delete(json); + goto exit; + } + + if (!cJSON_AddStringToObjLN(mountNode, "destination", "/opt/scope")) { + cJSON_Delete(mountNode); + cJSON_Delete(json); + goto exit; + } + + if (!cJSON_AddStringToObjLN(mountNode, "type", "bind")) { + cJSON_Delete(mountNode); + cJSON_Delete(json); + goto exit; + } + + if (!cJSON_AddStringToObjLN(mountNode, "source", "/tmp/appscope/dev/scope")) { + cJSON_Delete(mountNode); + cJSON_Delete(json); + goto exit; + } + + const char *optItems[2] = + { + "rbind", + "rprivate" + }; + + cJSON *optNodeArr = cJSON_CreateStringArray(optItems, 2); + if (!optNodeArr) { + cJSON_Delete(mountNode); + cJSON_Delete(json); + goto exit; + } + cJSON_AddItemToObject(mountNode, "options", optNodeArr); + cJSON_AddItemToArray(mountNodeArr, mountNode); + + /* + * Handle startContainer hooks process + * + "hooks":{ + "prestart":[ + { + "path":"/proc/1513/exe", + "args":[ + "libnetwork-setkey", + "-exec-root=/var/run/docker", + "6735578591bb3c5aebc91e5c702470c52d2c10cea52e4836604bf5a4a6c0f2eb", + "ec7e49ffc98c" + ] + } + ], + "startContainer":[ + { + "path":"/opt/scope" + "args":[ + "/opt/scope", + "extract", + "/opt/", + ] + }, + ] + */ + cJSON *hooksNode = cJSON_GetObjectItemCaseSensitive(json, "hooks"); + if (!hooksNode) { + hooksNode = cJSON_CreateObject(); + if (!hooksNode) { + cJSON_Delete(json); + goto exit; + } + cJSON_AddItemToObject(json, "hooks", hooksNode); + } + + cJSON *startContainerNodeArr = cJSON_GetObjectItemCaseSensitive(hooksNode, "startContainer"); + if (!startContainerNodeArr) { + startContainerNodeArr = cJSON_CreateArray(); + if (!startContainerNodeArr) { + cJSON_Delete(json); + goto exit; + } + cJSON_AddItemToObject(hooksNode, "startContainer", startContainerNodeArr); + } + + cJSON *startContainerNode = cJSON_CreateObject(); + if (!startContainerNode) { + cJSON_Delete(json); + goto exit; + } + + if (!cJSON_AddStringToObjLN(startContainerNode, "path", "/opt/scope")) { + cJSON_Delete(startContainerNode); + cJSON_Delete(json); + goto exit; + } + + const char *argsItems[3] = + { + "/opt/scope", + "extract", + "/opt" + }; + cJSON *argsNodeArr = cJSON_CreateStringArray(argsItems, 3); + if (!argsNodeArr) { + cJSON_Delete(startContainerNode); + cJSON_Delete(json); + goto exit; + } + cJSON_AddItemToObject(startContainerNode, "args", argsNodeArr); + cJSON_AddItemToArray(startContainerNodeArr, startContainerNode); + + char *jsonStr = cJSON_PrintUnformatted(json); + cJSON_Delete(json); + + // Overwrite the file + fp = scope_fopen(path, "w"); + if (fp == NULL) { + cJSON_free(jsonStr); + goto exit; + } + + scope_fprintf(fp, "%s\n", jsonStr); + + cJSON_free(jsonStr); + scope_fclose(fp); + +exit: + return; +} + +static void +containerStart(void) +{ + int i, argc; + char *buf; + const char *cWorkDir; + + if ((buf = scope_calloc(1, NCARGS)) == NULL) return; + + if ((argc = osGetArgv(g_proc.pid, buf, NCARGS)) == 0) { + scope_free(buf); + return; + } + + sysprint("Scope: found runc"); + + for (i = 0; buf[i]; i += scope_strlen(&buf[i]) + 1) { + char *arg = &buf[i]; + + if (arg) { + sysprint("\t%s:%d %s %d argv %s\n", __FUNCTION__, __LINE__, g_proc.procname, argc, arg); + + if (scope_strstr(arg, "--bundle")) { + // work dir for the container + cWorkDir = &buf[i + scope_strlen(arg) + 1]; + if (cWorkDir) sysprint("\t%s:%d container path %s\n", __FUNCTION__, __LINE__, cWorkDir); + break; + } + } + } + + if (cWorkDir) rewriteOpenContainersConfig(cWorkDir); + scope_free(buf); +} + static bool match_assy_instruction(void *addr, char *mnemonic) { @@ -532,10 +891,163 @@ getGoVersionAddr(const char* buf) return go_build_ver_addr; } +static Elf64_Addr +getSym12(const void *pclntab_addr, char *sname) +{ + if ((!pclntab_addr) || (!sname)) return 0; + + Elf64_Addr symaddr = 0; + uint64_t sym_count = *((const uint64_t *)(pclntab_addr + 8)); + const void *symtab_addr = pclntab_addr + 16; + + for (int i = 0; i < sym_count; i++) { + uint64_t func_offset = *((const uint64_t *)(symtab_addr + 8)); + uint32_t name_offset = *((const uint32_t *)(pclntab_addr + func_offset + 8)); + uint64_t sym_addr = *((const uint64_t *)(symtab_addr)); + const char *func_name = (const char *)(pclntab_addr + name_offset); + + if (scope_strcmp(sname, func_name) == 0) { + symaddr = sym_addr; + scopeLog(CFG_LOG_TRACE, "symbol found %s = 0x%08lx\n", func_name, sym_addr); + break; + } + + symtab_addr += 16; + } + + return symaddr; +} + +static Elf64_Addr +getSym16(const void *pclntab_addr, char *sname, char *altname, char *mnemonic) +{ + if ((!pclntab_addr) || (!sname)) return 0; + + Elf64_Addr symaddr = 0; + uint64_t sym_count = *((const uint64_t *)(pclntab_addr + 8)); + uint64_t funcnametab_offset = *((const uint64_t *)(pclntab_addr + (3 * 8))); + uint64_t pclntab_offset = *((const uint64_t *)(pclntab_addr + (7 * 8))); + const void *symtab_addr = pclntab_addr + pclntab_offset; + + for (int i = 0; i < sym_count; i++) { + uint64_t func_offset = *((const uint64_t *)(symtab_addr + 8)); + uint32_t name_offset = *((const uint32_t *)(pclntab_addr + pclntab_offset + func_offset + 8)); + uint64_t sym_addr = *((const uint64_t *)(symtab_addr)); + const char *func_name = (const char *)(pclntab_addr + funcnametab_offset + name_offset); + + if (scope_strcmp(sname, func_name) == 0) { + symaddr = sym_addr; + scopeLog(CFG_LOG_TRACE, "symbol found %s = 0x%08lx\n", func_name, sym_addr); + break; + } + + // In go 1.17+ we need to ensure we find the correct symbol in the case of ambiguity + if (altname && mnemonic && + (scope_strcmp(altname, func_name) == 0) && + (match_assy_instruction((void *)sym_addr, mnemonic) == TRUE)) { + symaddr = sym_addr; + break; + } + + symtab_addr += 16; + } + + return symaddr; +} + +static Elf64_Addr +getSym1820(const void *pclntab_addr, char *sname, char *altname, char *mnemonic) +{ + if ((!pclntab_addr) || (!sname)) return 0; + + Elf64_Addr symaddr = 0; + uint64_t sym_count = *((const uint64_t *)(pclntab_addr + 8)); + // In go 1.18 the funcname table and the pcln table are stored in the text section + uint64_t text_start = *((const uint64_t *)(pclntab_addr + (3 * 8))); + uint64_t funcnametab_offset = *((const uint64_t *)(pclntab_addr + (4 * 8))); + //uint64_t funcnametab_addr = (uint64_t)(funcnametab_offset + pclntab_addr); + uint64_t pclntab_offset = *((const uint64_t *)(pclntab_addr + (8 * 8))); + // A "symbtab" is an entry in the pclntab, probably better known as a pcln + const void *symtab_addr = (const void *)(pclntab_addr + pclntab_offset); + + for (int i = 0; i < sym_count; i++) { + uint32_t func_offset = *((uint32_t *)(symtab_addr + 4)); + uint32_t name_offset = *((const uint32_t *)(pclntab_addr + pclntab_offset + func_offset + 4)); + func_offset = *((uint32_t *)(symtab_addr)); + uint64_t sym_addr = (uint64_t)(func_offset + text_start); + const char *func_name = (const char *)(pclntab_addr + funcnametab_offset + name_offset); + if (scope_strcmp(sname, func_name) == 0) { + symaddr = sym_addr; + scopeLog(CFG_LOG_ERROR, "symbol found %s = 0x%08lx\n", func_name, sym_addr); + break; + } + + // In go 1.17+ we need to ensure we find the correct symbol in the case of ambiguity + if (altname && mnemonic && + (scope_strcmp(altname, func_name) == 0) && + (match_assy_instruction((void *)sym_addr, mnemonic) == TRUE)) { + symaddr = sym_addr; + break; + } + + symtab_addr += 8; + } + + return symaddr; +} + +static Elf64_Addr +embedPclntab(const char *buf, char *sname, char *altname, char *mnemonic) +{ + Elf64_Addr symaddr = 0; + Elf64_Ehdr *ehdr = (Elf64_Ehdr *)buf; + Elf64_Shdr *sections = (Elf64_Shdr *)(buf + ehdr->e_shoff); + const char *section_strtab = (char *)buf + sections[ehdr->e_shstrndx].sh_offset; + + for (int i = 0; i < ehdr->e_shnum; i++) { + const char *sec_name = section_strtab + sections[i].sh_name; + if (scope_strstr(sec_name, "data.rel.ro") != 0) { + + const void *pclntab_addr = buf + sections[i].sh_offset; + unsigned char *data = (unsigned char *)pclntab_addr; + size_t slen = sections[i].sh_size; + size_t j; + + // Find the magic number in the pclntab header + for (j = 0; j <= slen; j += 4) { //0x3c8c80 + if (((data[j] == 0xf1) || (data[j] == 0xf0) || + (data[j] == 0xfa) || (data[j] == 0xfb)) && + data[j+1] == 0xff && + data[j+2] == 0xff && + data[j+3] == 0xff) { + //sysprint("%s:%d pclntab was recognized at %p\n", + // __FUNCTION__, __LINE__, &data[j]); + + switch (data[j]) { + // Go 18 - 20 + case 0xf1: + case 0xf0: + return getSym1820(&data[j], sname, altname, mnemonic); + // Go 16 + case 0xfa: + return getSym16(&data[j], sname, altname, mnemonic); + // Go 12 + case 0xfb: + return getSym12(&data[j], sname); + } + } + } + } + } + + return symaddr; +} + static void * getGoSymbol(const char *buf, char *sname, char *altname, char *mnemonic) { int i; + bool found = FALSE; Elf64_Addr symaddr = 0; Elf64_Ehdr *ehdr; Elf64_Shdr *sections; @@ -551,87 +1063,19 @@ getGoSymbol(const char *buf, char *sname, char *altname, char *mnemonic) for (i = 0; i < ehdr->e_shnum; i++) { sec_name = section_strtab + sections[i].sh_name; if (scope_strstr(sec_name, ".gopclntab")) { + found = TRUE; const void *pclntab_addr = buf + sections[i].sh_offset; /* - Go symbol table is stored in the .gopclntab section - More info: https://docs.google.com/document/d/1lyPIbmsYbXnpNj57a261hgOYVpNRcgydurVQIyZOz_o/pub - */ + * The Go symbol table is stored in the .gopclntab section + * More info: https://docs.google.com/document/d/1lyPIbmsYbXnpNj57a261hgOYVpNRcgydurVQIyZOz_o/pub + */ uint32_t magic = *((const uint32_t *)(pclntab_addr)); if (magic == GOPCLNTAB_MAGIC_112) { - uint64_t sym_count = *((const uint64_t *)(pclntab_addr + 8)); - const void *symtab_addr = pclntab_addr + 16; - - for(i=0; iassembly_fn == go_hook_reg_syscall6) { g_syscall6_return = (uint64_t)tap->return_addr; } - break; // Done patching } @@ -909,13 +1357,50 @@ go_version_numbers(const char *go_runtime_version) g_go_maint_ver = maint_val; } +/* + * Some Go executables are built such that the .abi0 + * extension is used for internal runtime symbols. Others + * emit symbols without the version extension. So, if we + * can't resolve the symbol without a version extension, we + * try the symbol with extension. + */ +static void * +tryAbi0(const char *buf, char *sname) +{ + if (!buf || !sname) return NULL; + + size_t slen = scope_strlen(sname); + if (slen == 0) return NULL; + + void *funcaddr = NULL; + char abi0name[slen + sizeof(".abi0 ")]; + + scope_memset(abi0name, 0, sizeof(abi0name)); + scope_strncpy(abi0name, sname, slen); + scope_strcat(abi0name, ".abi0"); + + funcprint("%s:%d %s (%ld) %s\n", __FUNCTION__, __LINE__, sname, slen, abi0name); + + // Look for the symbol in the elf strtab, then meta data; .gopclntab section + if (((funcaddr = getSymbol(buf, abi0name)) == 0) && + ((funcaddr = getGoSymbol(buf, abi0name, NULL, NULL)) == 0)) { + return NULL; + } + + return funcaddr; +} + void initGoHook(elf_buf_t *ebuf) { + if (!ebuf || !ebuf->buf) return; + int rc; funchook_t *funchook; char *go_ver; char *go_runtime_version = NULL; + uint64_t base = 0LL; + //Elf64_Ehdr *ehdr = (Elf64_Ehdr *)ebuf->buf; funchook = funchook_create(); @@ -924,8 +1409,9 @@ initGoHook(elf_buf_t *ebuf) funchook_set_debug_file(DEFAULT_LOG_PATH); } - // default to a dynamic app? - if (checkEnv("SCOPE_EXEC_TYPE", "static")) { + // check ELF type + //if (checkEnv("SCOPE_EXEC_TYPE", "static")) { + if (is_static(ebuf->buf)) { scopeSetGoAppStateStatic(TRUE); //patchClone(); sysprint("This is a static app\n"); @@ -934,11 +1420,9 @@ initGoHook(elf_buf_t *ebuf) sysprint("This is a dynamic app\n"); } - //check ELF type - Elf64_Ehdr *ehdr = (Elf64_Ehdr *)ebuf->buf; // if it's a position independent executable, get the base address from /proc/self/maps - uint64_t base = 0LL; - if (ehdr->e_type == ET_DYN && (scopeGetGoAppStateStatic() == FALSE)) { + // default to a dynamic app? + if (scopeGetGoAppStateStatic() == FALSE) { if (osGetBaseAddr(&base) == FALSE) { sysprint("ERROR: can't get the base address\n"); funchook_destroy(funchook); @@ -959,10 +1443,10 @@ initGoHook(elf_buf_t *ebuf) if (g_go_build_ver[0] != '\0') { go_ver = (char *)((uint64_t)ver_addr); } else { - go_ver = go_str((void *)((uint64_t)ver_addr + base)); + go_ver = go_str((void *)((uint64_t)ver_addr + base), FALSE); } } else { - go_ver = go_str((void *)((uint64_t)go_ver_sym + base)); + go_ver = go_str((void *)((uint64_t)go_ver_sym + base), FALSE); } if (go_ver && (go_runtime_version = go_ver)) { @@ -989,6 +1473,10 @@ initGoHook(elf_buf_t *ebuf) return; // don't install our hooks } + if (scope_strstr(g_proc.procname, "runc") != NULL) { + containerStart(); + } + uint64_t *ReadFrame_addr; if (((ReadFrame_addr = getSymbol(ebuf->buf, "net/http.(*http2Framer).ReadFrame")) == 0) && ((ReadFrame_addr = getGoSymbol(ebuf->buf, "net/http.(*http2Framer).ReadFrame", NULL, NULL)) == 0)) { @@ -1046,8 +1534,12 @@ initGoHook(elf_buf_t *ebuf) void *orig_func; // Look for the symbol in the ELF symbol table if (((orig_func = getSymbol(ebuf->buf, tap->func_name)) == NULL) && - // Otherwise look in the .gopclntab section - ((orig_func = getGoSymbol(ebuf->buf, tap->func_name, NULL, NULL)) == NULL)) { + // look in the .gopclntab section + ((orig_func = getGoSymbol(ebuf->buf, tap->func_name, NULL, NULL)) == NULL) && + // check dynamic symbols; exec has been stripped + ((orig_func = getDynSymbol(ebuf->buf, tap->func_name)) == NULL) && + // is the symbol defined as an original API; with an abi0 extension + ((orig_func = tryAbi0(ebuf->buf, tap->func_name)) == NULL)) { sysprint("WARN: can't get the address for %s\n", tap->func_name); continue; } diff --git a/test/data/filter_0.yml b/test/data/filter/filter_0.yml similarity index 100% rename from test/data/filter_0.yml rename to test/data/filter/filter_0.yml diff --git a/test/data/filter_1.yml b/test/data/filter/filter_1.yml similarity index 100% rename from test/data/filter_1.yml rename to test/data/filter/filter_1.yml diff --git a/test/data/filter_2.yml b/test/data/filter/filter_2.yml similarity index 100% rename from test/data/filter_2.yml rename to test/data/filter/filter_2.yml diff --git a/test/data/filter_3.yml b/test/data/filter/filter_3.yml similarity index 100% rename from test/data/filter_3.yml rename to test/data/filter/filter_3.yml diff --git a/test/data/filter_4.yml b/test/data/filter/filter_4.yml similarity index 100% rename from test/data/filter_4.yml rename to test/data/filter/filter_4.yml diff --git a/test/data/filter_5.yml b/test/data/filter/filter_5.yml similarity index 100% rename from test/data/filter_5.yml rename to test/data/filter/filter_5.yml diff --git a/test/data/filter_6.yml b/test/data/filter/filter_6.yml similarity index 100% rename from test/data/filter_6.yml rename to test/data/filter/filter_6.yml diff --git a/test/data/oci/oci0in.json b/test/data/oci/oci0in.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/test/data/oci/oci0in.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/data/oci/oci0out.json b/test/data/oci/oci0out.json new file mode 100644 index 000000000..2084079ad --- /dev/null +++ b/test/data/oci/oci0out.json @@ -0,0 +1,17 @@ +{ + "process": { + "env": ["LD_PRELOAD=/opt/libscope.so", "SCOPE_SETUP_DONE=true"] + }, + "mounts": [{ + "destination": "/opt/scope", + "type": "bind", + "source": "/tmp/appscope/dev/scope", + "options": ["rbind", "rprivate"] + }], + "hooks": { + "startContainer": [{ + "path": "/opt/scope", + "args": ["/opt/scope", "extract", "/opt"] + }] + } +} \ No newline at end of file diff --git a/test/data/oci/oci1in.json b/test/data/oci/oci1in.json new file mode 100644 index 000000000..adff95a01 --- /dev/null +++ b/test/data/oci/oci1in.json @@ -0,0 +1,4 @@ +{ + "process": { + } +} diff --git a/test/data/oci/oci1out.json b/test/data/oci/oci1out.json new file mode 100644 index 000000000..2084079ad --- /dev/null +++ b/test/data/oci/oci1out.json @@ -0,0 +1,17 @@ +{ + "process": { + "env": ["LD_PRELOAD=/opt/libscope.so", "SCOPE_SETUP_DONE=true"] + }, + "mounts": [{ + "destination": "/opt/scope", + "type": "bind", + "source": "/tmp/appscope/dev/scope", + "options": ["rbind", "rprivate"] + }], + "hooks": { + "startContainer": [{ + "path": "/opt/scope", + "args": ["/opt/scope", "extract", "/opt"] + }] + } +} \ No newline at end of file diff --git a/test/data/oci/oci2in.json b/test/data/oci/oci2in.json new file mode 100644 index 000000000..8f7c00b26 --- /dev/null +++ b/test/data/oci/oci2in.json @@ -0,0 +1,8 @@ +{ + "process": { + "env":[ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "LD_PRELOAD=/usr/test.so" + ] + } +} diff --git a/test/data/oci/oci2out.json b/test/data/oci/oci2out.json new file mode 100644 index 000000000..5d4920535 --- /dev/null +++ b/test/data/oci/oci2out.json @@ -0,0 +1,17 @@ +{ + "process": { + "env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "LD_PRELOAD=/opt/libscope.so:/usr/test.so", "SCOPE_SETUP_DONE=true"] + }, + "mounts": [{ + "destination": "/opt/scope", + "type": "bind", + "source": "/tmp/appscope/dev/scope", + "options": ["rbind", "rprivate"] + }], + "hooks": { + "startContainer": [{ + "path": "/opt/scope", + "args": ["/opt/scope", "extract", "/opt"] + }] + } +} \ No newline at end of file diff --git a/test/data/oci/oci3in.json b/test/data/oci/oci3in.json new file mode 100644 index 000000000..2543748fe --- /dev/null +++ b/test/data/oci/oci3in.json @@ -0,0 +1,7 @@ +{ + "process": { + "env":[ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ] + } +} diff --git a/test/data/oci/oci3out.json b/test/data/oci/oci3out.json new file mode 100644 index 000000000..f1146e795 --- /dev/null +++ b/test/data/oci/oci3out.json @@ -0,0 +1,17 @@ +{ + "process": { + "env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "LD_PRELOAD=/opt/libscope.so", "SCOPE_SETUP_DONE=true"] + }, + "mounts": [{ + "destination": "/opt/scope", + "type": "bind", + "source": "/tmp/appscope/dev/scope", + "options": ["rbind", "rprivate"] + }], + "hooks": { + "startContainer": [{ + "path": "/opt/scope", + "args": ["/opt/scope", "extract", "/opt"] + }] + } +} \ No newline at end of file diff --git a/test/data/oci/oci4in.json b/test/data/oci/oci4in.json new file mode 100644 index 000000000..b14955367 --- /dev/null +++ b/test/data/oci/oci4in.json @@ -0,0 +1,19 @@ +{ + "process": { + "env":[ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ] + }, + "mounts":[ + { + "destination":"/proc", + "type":"proc", + "source":"proc", + "options":[ + "nosuid", + "noexec", + "nodev" + ] + } + ] +} diff --git a/test/data/oci/oci4out.json b/test/data/oci/oci4out.json new file mode 100644 index 000000000..14cfed513 --- /dev/null +++ b/test/data/oci/oci4out.json @@ -0,0 +1,22 @@ +{ + "process": { + "env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "LD_PRELOAD=/opt/libscope.so", "SCOPE_SETUP_DONE=true"] + }, + "mounts": [{ + "destination": "/proc", + "type": "proc", + "source": "proc", + "options": ["nosuid", "noexec", "nodev"] + }, { + "destination": "/opt/scope", + "type": "bind", + "source": "/tmp/appscope/dev/scope", + "options": ["rbind", "rprivate"] + }], + "hooks": { + "startContainer": [{ + "path": "/opt/scope", + "args": ["/opt/scope", "extract", "/opt"] + }] + } +} \ No newline at end of file diff --git a/test/data/oci/oci5in.json b/test/data/oci/oci5in.json new file mode 100644 index 000000000..bd048501d --- /dev/null +++ b/test/data/oci/oci5in.json @@ -0,0 +1,21 @@ +{ + "process": { + "env":[ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ] + }, + "mounts":[ + { + "destination":"/proc", + "type":"proc", + "source":"proc", + "options":[ + "nosuid", + "noexec", + "nodev" + ] + } + ], + "hooks":{ + } +} diff --git a/test/data/oci/oci5out.json b/test/data/oci/oci5out.json new file mode 100644 index 000000000..14cfed513 --- /dev/null +++ b/test/data/oci/oci5out.json @@ -0,0 +1,22 @@ +{ + "process": { + "env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "LD_PRELOAD=/opt/libscope.so", "SCOPE_SETUP_DONE=true"] + }, + "mounts": [{ + "destination": "/proc", + "type": "proc", + "source": "proc", + "options": ["nosuid", "noexec", "nodev"] + }, { + "destination": "/opt/scope", + "type": "bind", + "source": "/tmp/appscope/dev/scope", + "options": ["rbind", "rprivate"] + }], + "hooks": { + "startContainer": [{ + "path": "/opt/scope", + "args": ["/opt/scope", "extract", "/opt"] + }] + } +} \ No newline at end of file diff --git a/test/data/oci/oci6in.json b/test/data/oci/oci6in.json new file mode 100644 index 000000000..f9f8e47f7 --- /dev/null +++ b/test/data/oci/oci6in.json @@ -0,0 +1,29 @@ +{ + "process": { + "env":[ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ] + }, + "mounts":[ + { + "destination":"/proc", + "type":"proc", + "source":"proc", + "options":[ + "nosuid", + "noexec", + "nodev" + ] + } + ], + "hooks":{ + "containerstatrt":[ + { + "path":"foo", + "args":[ + "bar" + ] + } + ] + } +} diff --git a/test/data/oci/oci6out.json b/test/data/oci/oci6out.json new file mode 100644 index 000000000..2b1761ad1 --- /dev/null +++ b/test/data/oci/oci6out.json @@ -0,0 +1,26 @@ +{ + "process": { + "env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "LD_PRELOAD=/opt/libscope.so", "SCOPE_SETUP_DONE=true"] + }, + "mounts": [{ + "destination": "/proc", + "type": "proc", + "source": "proc", + "options": ["nosuid", "noexec", "nodev"] + }, { + "destination": "/opt/scope", + "type": "bind", + "source": "/tmp/appscope/dev/scope", + "options": ["rbind", "rprivate"] + }], + "hooks": { + "containerstatrt": [{ + "path": "foo", + "args": ["bar"] + }], + "startContainer": [{ + "path": "/opt/scope", + "args": ["/opt/scope", "extract", "/opt"] + }] + } +} \ No newline at end of file diff --git a/test/manual/gosym/gosym/gosym.c b/test/manual/gosym/gosym/gosym.c index fba5e3841..da220de32 100644 --- a/test/manual/gosym/gosym/gosym.c +++ b/test/manual/gosym/gosym/gosym.c @@ -23,6 +23,8 @@ gcc -o gosym ./test/manual/gosym.c #define GOPCLNTAB_MAGIC_118 0xfffffff0 #define GOPCLNTAB_MAGIC_120 0xfffffff1 +#define TRUE 1 +#define FALSE 0 #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) struct sym_status { @@ -85,7 +87,128 @@ static void printSymStatus(void) { } } -int printSymbols(const char *fname) +static void +getSym12(void *pclntab_addr) +{ + uint64_t sym_count = *((const uint64_t *)(pclntab_addr + 8)); + const void *symtab_addr = pclntab_addr + 16; + printf_info("Symbol count = %ld\n", sym_count); + printf_info("Address\t\tSymbol Name\n"); + printf_info("---------------------------\n"); + for (int i = 0; i < sym_count; i++) { + uint64_t sym_addr = *((const uint64_t *)(symtab_addr)); + uint64_t func_offset = *((const uint64_t *)(symtab_addr + 8)); + uint32_t name_offset = *((const uint32_t *)(pclntab_addr + func_offset + 8)); + const char *func_name = (const char *)(pclntab_addr + name_offset); + printf_info("0x%lx\t%s\n", sym_addr, func_name); + + symtab_addr += 16; + updateSymStatus(func_name); + } +} + +static void +getSym16(void *pclntab_addr) +{ + uint64_t sym_count = *((const uint64_t *)(pclntab_addr + 8)); + + uint64_t funcnametab_offset = *((const uint64_t *)(pclntab_addr + (3 * 8))); + uint64_t pclntab_offset = *((const uint64_t *)(pclntab_addr + (7 * 8))); + + const void *symtab_addr = pclntab_addr + pclntab_offset; + printf_info("Symbol count = %ld\n", sym_count); + printf_info("Address\t\tSymbol Name\n"); + printf_info("---------------------------\n"); + for (int i = 0; i < sym_count; i++) { + uint64_t sym_addr = *((const uint64_t *)(symtab_addr)); + uint64_t func_offset = *((const uint64_t *)(symtab_addr + 8)); + uint32_t name_offset = *((const uint32_t *)(pclntab_addr + pclntab_offset + func_offset + 8)); + const char *func_name = (const char *)(pclntab_addr + funcnametab_offset + name_offset); + printf_info("0x%lx\t%s\n", sym_addr, func_name); + + symtab_addr += 16; + updateSymStatus(func_name); + } +} + +static void +getSym1820(void *pclntab_addr) +{ + uint64_t sym_count = *((const uint64_t *)(pclntab_addr + 8)); + uint64_t funcnametab_offset = *((const uint64_t *)(pclntab_addr + (4 * 8))); + uint64_t pclntab_offset = *((const uint64_t *)(pclntab_addr + (8 * 8))); + uint64_t text_start = *((const uint64_t *)(pclntab_addr + (3 * 8))); + + const void *symtab_addr = pclntab_addr + pclntab_offset; + + printf_info("Symbol count = %ld\n", sym_count); + printf_info("Address\t\tSymbol Name\n"); + printf_info("---------------------------\n"); + for (int i = 0; i < sym_count; i++) { + uint32_t func_offset = *((uint32_t *)(symtab_addr + 4)); + uint32_t name_offset = *((const uint32_t *)(pclntab_addr + pclntab_offset + func_offset + 4)); + func_offset = *((uint32_t *)(symtab_addr)); + uint64_t sym_addr = (uint64_t)(func_offset + text_start); + const char *func_name = (const char *)(pclntab_addr + funcnametab_offset + name_offset); + printf_info("0x%lx\t%s\n", sym_addr, func_name); + symtab_addr += 8; + updateSymStatus(func_name); + } +} + +static bool +embedPclntab(uint8_t *buf) +{ + Elf64_Ehdr *ehdr = (Elf64_Ehdr *)buf; + Elf64_Shdr *sections = (Elf64_Shdr *)(buf + ehdr->e_shoff); + const char *section_strtab = (char *)buf + sections[ehdr->e_shstrndx].sh_offset; + + for (int i = 0; i < ehdr->e_shnum; i++) { + const char *sec_name = section_strtab + sections[i].sh_name; + //printf("%s:%d %s\n", __FUNCTION__, __LINE__, sec_name); + if (strstr(sec_name, "data.rel.ro") != 0) { + + const void *pclntab_addr = buf + sections[i].sh_offset; + unsigned char *data = (char *)pclntab_addr; + size_t slen = sections[i].sh_size; + size_t j; + + //printf("%s:%d FOUND at %p %p %p\n", __FUNCTION__, __LINE__, + // buf, data, pclntab_addr); + // Find the magic number in the pclntab header + for (j = 0; j <= slen; j += 4) { //0x3c8c80 + if (((data[j] == 0xf1) || (data[j] == 0xf0) || + (data[j] == 0xfa) || (data[j] == 0xfb)) && + data[j+1] == 0xff && + data[j+2] == 0xff && + data[j+3] == 0xff) { + printf_info("%s:%d pclntab was recognized at %p\n", + __FUNCTION__, __LINE__, &data[j]); + + switch (data[j]) { + // Go 18 and 20 + case 0xf1: + case 0xf0: + getSym1820(&data[j]); + return TRUE; + // Go 16 + case 0xfa: + getSym16(&data[j]); + return TRUE; + // Go 12 + case 0xfb: + getSym12(&data[j]); + return TRUE; + } + } + } + } + } + return FALSE; +} + +int +printSymbols(const char *fname) { int fd; struct stat st; @@ -118,35 +241,23 @@ int printSymbols(const char *fname) Elf64_Shdr *sections = (Elf64_Shdr *)(buf + ehdr->e_shoff); const char *section_strtab = (char *)buf + sections[ehdr->e_shstrndx].sh_offset; + bool found = FALSE; for (int i = 0; i < ehdr->e_shnum; i++) { const char *sec_name = section_strtab + sections[i].sh_name; if (strcmp(".gopclntab", sec_name) == 0) { - const void *pclntab_addr = buf + sections[i].sh_offset; + found = TRUE; + void *pclntab_addr = buf + sections[i].sh_offset; /* Go symbol table is stored in the .gopclntab section More info: https://docs.google.com/document/d/1lyPIbmsYbXnpNj57a261hgOYVpNRcgydurVQIyZOz_o/pub */ uint32_t magic = *((const uint32_t *)(pclntab_addr)); if (magic == GOPCLNTAB_MAGIC_112) { - printf_validate("[INFO] gopclntab was recognized\n"); - uint64_t sym_count = *((const uint64_t *)(pclntab_addr + 8)); - const void *symtab_addr = pclntab_addr + 16; - printf_info("Symbol count = %ld\n", sym_count); - printf_info("Address\t\tSymbol Name\n"); - printf_info("---------------------------\n"); - for (i = 0; i < sym_count; i++) { - uint64_t sym_addr = *((const uint64_t *)(symtab_addr)); - uint64_t func_offset = *((const uint64_t *)(symtab_addr + 8)); - uint32_t name_offset = *((const uint32_t *)(pclntab_addr + func_offset + 8)); - const char *func_name = (const char *)(pclntab_addr + name_offset); - printf_info("0x%lx\t%s\n", sym_addr, func_name); - - symtab_addr += 16; - updateSymStatus(func_name); - } + printf_validate("[INFO] gopclntab for 12 was recognized\n"); + getSym12(pclntab_addr); } else if (magic == GOPCLNTAB_MAGIC_116) { - printf_validate("[INFO] gopclntab was recognized\n"); + printf_validate("[INFO] gopclntab for 16 was recognized\n"); // the layout of pclntab: // // .gopclntab/__gopclntab [elf/macho section] @@ -182,47 +293,10 @@ int printSymbols(const char *fname) // function table, alternating PC and offset to func struct [each entry thearch.ptrsize bytes] // end PC [thearch.ptrsize bytes] // func structures, pcdata offsets, func data. - uint64_t sym_count = *((const uint64_t *)(pclntab_addr + 8)); - - uint64_t funcnametab_offset = *((const uint64_t *)(pclntab_addr + (3 * 8))); - uint64_t pclntab_offset = *((const uint64_t *)(pclntab_addr + (7 * 8))); - - const void *symtab_addr = pclntab_addr + pclntab_offset; - printf_info("Symbol count = %ld\n", sym_count); - printf_info("Address\t\tSymbol Name\n"); - printf_info("---------------------------\n"); - for (i = 0; i < sym_count; i++) { - uint64_t sym_addr = *((const uint64_t *)(symtab_addr)); - uint64_t func_offset = *((const uint64_t *)(symtab_addr + 8)); - uint32_t name_offset = *((const uint32_t *)(pclntab_addr + pclntab_offset + func_offset + 8)); - const char *func_name = (const char *)(pclntab_addr + funcnametab_offset + name_offset); - printf_info("0x%lx\t%s\n", sym_addr, func_name); - - symtab_addr += 16; - updateSymStatus(func_name); - } + getSym16(pclntab_addr); } else if ((magic == GOPCLNTAB_MAGIC_118) || (magic == GOPCLNTAB_MAGIC_120)) { - printf_validate("[INFO] gopclntab was recognized\n"); - uint64_t sym_count = *((const uint64_t *)(pclntab_addr + 8)); - uint64_t funcnametab_offset = *((const uint64_t *)(pclntab_addr + (4 * 8))); - uint64_t pclntab_offset = *((const uint64_t *)(pclntab_addr + (8 * 8))); - uint64_t text_start = *((const uint64_t *)(pclntab_addr + (3 * 8))); - - const void *symtab_addr = pclntab_addr + pclntab_offset; - - printf_info("Symbol count = %ld\n", sym_count); - printf_info("Address\t\tSymbol Name\n"); - printf_info("---------------------------\n"); - for (i = 0; i < sym_count; i++) { - uint32_t func_offset = *((uint32_t *)(symtab_addr + 4)); - uint32_t name_offset = *((const uint32_t *)(pclntab_addr + pclntab_offset + func_offset + 4)); - func_offset = *((uint32_t *)(symtab_addr)); - uint64_t sym_addr = (uint64_t)(func_offset + text_start); - const char *func_name = (const char *)(pclntab_addr + funcnametab_offset + name_offset); - printf_info("0x%lx\t%s\n", sym_addr, func_name); - symtab_addr += 8; - updateSymStatus(func_name); - } + printf_validate("[INFO] gopclntab for 18/20 was recognized\n"); + getSym1820(pclntab_addr); } else { fprintf(stderr, "[ERROR] Unknown header in .gopclntab\n"); munmap(buf, st.st_size); @@ -233,6 +307,14 @@ int printSymbols(const char *fname) } } + // if no .gopclntab section was found, check embedded + if ((found == FALSE) && (embedPclntab(buf) == FALSE)) { + fprintf(stderr, "[ERROR] cannot locate a pclntab\n"); + munmap(buf, st.st_size); + close(fd); + return -1; + } + printSymStatus(); munmap(buf, st.st_size); diff --git a/test/unit/execute.sh b/test/unit/execute.sh index 4442777aa..9ca2dce04 100755 --- a/test/unit/execute.sh +++ b/test/unit/execute.sh @@ -69,6 +69,7 @@ run_test test/${OS}/coredumptest run_test test/${OS}/ipctest run_test test/${OS}/snapshottest run_test test/${OS}/ostest +run_test test/${OS}/ocitest run_test test/${OS}/strsettest run_test test/${OS}/cfgutilstest run_test test/${OS}/cfgtest diff --git a/test/unit/library/cfgutilstest.c b/test/unit/library/cfgutilstest.c index 5e22c8f53..e9c4c3c5a 100644 --- a/test/unit/library/cfgutilstest.c +++ b/test/unit/library/cfgutilstest.c @@ -2587,7 +2587,7 @@ cfgReadCustomAnchorExtend(void **state) static void filterEmptyProcName(void **state) { char path[PATH_MAX] = {0}; - scope_snprintf(path, sizeof(path), "%s/data/filter_0.yml", dirPath); + scope_snprintf(path, sizeof(path), "%s/data/filter/filter_0.yml", dirPath); config_t *cfg = cfgCreateDefault(); assert_non_null(cfg); filter_status_t res = cfgFilterStatus(NULL, "foo", testAccessFilterPath(path), cfg); @@ -2601,7 +2601,7 @@ filterEmptyProcName(void **state) { static void filterEmptyProcCmdLine(void **state) { char path[PATH_MAX] = {0}; - scope_snprintf(path, sizeof(path), "%s/data/filter_0.yml", dirPath); + scope_snprintf(path, sizeof(path), "%s/data/filter/filter_0.yml", dirPath); config_t *cfg = cfgCreateDefault(); assert_non_null(cfg); filter_status_t res = cfgFilterStatus("foo", NULL, testAccessFilterPath(path), cfg); @@ -2633,7 +2633,7 @@ filterNullFilterPath(void **state) { static void filterNullCfg(void **state) { char path[PATH_MAX] = {0}; - scope_snprintf(path, sizeof(path), "%s/data/filter_0.yml", dirPath); + scope_snprintf(path, sizeof(path), "%s/data/filter/filter_0.yml", dirPath); filter_status_t res = cfgFilterStatus("foo", "foo", testAccessFilterPath(path), NULL); assert_int_equal(res, FILTER_ERROR); dbgInit(); // reset dbg for the rest of the tests @@ -2644,7 +2644,7 @@ filterNullCfg(void **state) { static void filterNonExistingFilterFile(void **state) { char path[PATH_MAX] = {0}; - scope_snprintf(path, sizeof(path), "%s/data/filter_non_existing.yml", dirPath); + scope_snprintf(path, sizeof(path), "%s/data/filter/filter_non_existing.yml", dirPath); config_t *cfg = cfgCreateDefault(); assert_non_null(cfg); filter_status_t res = cfgFilterStatus("foo", "foo", testAccessFilterPath(path), cfg); @@ -2657,7 +2657,7 @@ filterNonExistingFilterFile(void **state) { static void filterProcNameAllowListPresent(void **state) { char path[PATH_MAX] = {0}; - scope_snprintf(path, sizeof(path), "%s/data/filter_0.yml", dirPath); + scope_snprintf(path, sizeof(path), "%s/data/filter/filter_0.yml", dirPath); config_t *cfg = cfgCreateDefault(); assert_non_null(cfg); filter_status_t res = cfgFilterStatus("redis", "", testAccessFilterPath(path), cfg); @@ -2670,7 +2670,7 @@ filterProcNameAllowListPresent(void **state) { static void filterProcNameDenyListPresent(void **state) { char path[PATH_MAX] = {0}; - scope_snprintf(path, sizeof(path), "%s/data/filter_0.yml", dirPath); + scope_snprintf(path, sizeof(path), "%s/data/filter/filter_0.yml", dirPath); config_t *cfg = cfgCreateDefault(); assert_non_null(cfg); filter_status_t res = cfgFilterStatus("git", "", testAccessFilterPath(path), cfg); @@ -2683,7 +2683,7 @@ filterProcNameDenyListPresent(void **state) { static void filterArgAllowListPresent(void **state) { char path[PATH_MAX] = {0}; - scope_snprintf(path, sizeof(path), "%s/data/filter_1.yml", dirPath); + scope_snprintf(path, sizeof(path), "%s/data/filter/filter_1.yml", dirPath); config_t *cfg = cfgCreateDefault(); assert_non_null(cfg); filter_status_t res = cfgFilterStatus("", "redis arg1", testAccessFilterPath(path), cfg); @@ -2696,7 +2696,7 @@ filterArgAllowListPresent(void **state) { static void filterArgDenyListPresent(void **state) { char path[PATH_MAX] = {0}; - scope_snprintf(path, sizeof(path), "%s/data/filter_1.yml", dirPath); + scope_snprintf(path, sizeof(path), "%s/data/filter/filter_1.yml", dirPath); config_t *cfg = cfgCreateDefault(); assert_non_null(cfg); filter_status_t res = cfgFilterStatus("", "git arg1", testAccessFilterPath(path), cfg); @@ -2709,13 +2709,13 @@ filterArgDenyListPresent(void **state) { static void filterArgAllowListPartFindPresent(void **state) { char path[PATH_MAX] = {0}; - scope_snprintf(path, sizeof(path), "%s/data/filter_0.yml", dirPath); + scope_snprintf(path, sizeof(path), "%s/data/filter/filter_0.yml", dirPath); config_t *cfg = cfgCreateDefault(); assert_non_null(cfg); filter_status_t res = cfgFilterStatus("redis-server", "redis-server", testAccessFilterPath(path), cfg); assert_int_equal(res, FILTER_NOT_SCOPED); scope_memset(path, 0, sizeof(path)); - scope_snprintf(path, sizeof(path), "%s/data/filter_1.yml", dirPath); + scope_snprintf(path, sizeof(path), "%s/data/filter/filter_1.yml", dirPath); res = cfgFilterStatus("redis-server", "redis-server", testAccessFilterPath(path), cfg); assert_int_equal(res, FILTER_SCOPED_WITH_CFG); // cleanup @@ -2726,7 +2726,7 @@ filterArgAllowListPartFindPresent(void **state) { static void filterArgAllowListEmptyProcMissing(void **state) { char path[PATH_MAX] = {0}; - scope_snprintf(path, sizeof(path), "%s/data/filter_0.yml", dirPath); + scope_snprintf(path, sizeof(path), "%s/data/filter/filter_0.yml", dirPath); config_t *cfg = cfgCreateDefault(); assert_non_null(cfg); @@ -2740,7 +2740,7 @@ filterArgAllowListEmptyProcMissing(void **state) { static void filterArgAllowListNotEmptyProcMissing(void **state) { char path[PATH_MAX] = {0}; - scope_snprintf(path, sizeof(path), "%s/data/filter_2.yml", dirPath); + scope_snprintf(path, sizeof(path), "%s/data/filter/filter_2.yml", dirPath); config_t *cfg = cfgCreateDefault(); assert_non_null(cfg); filter_status_t res = cfgFilterStatus("memcached", "memcached", testAccessFilterPath(path), cfg); @@ -2748,7 +2748,7 @@ filterArgAllowListNotEmptyProcMissing(void **state) { scope_memset(path, 0, PATH_MAX); - scope_snprintf(path, sizeof(path), "%s/data/filter_3.yml", dirPath); + scope_snprintf(path, sizeof(path), "%s/data/filter/filter_3.yml", dirPath); res = cfgFilterStatus("memcached", "memcached", testAccessFilterPath(path), cfg); assert_int_equal(res, FILTER_NOT_SCOPED); // cleanup @@ -2759,7 +2759,7 @@ filterArgAllowListNotEmptyProcMissing(void **state) { static void filterVerifyCfg(void **state) { char path[PATH_MAX] = {0}; - scope_snprintf(path, sizeof(path), "%s/data/filter_0.yml", dirPath); + scope_snprintf(path, sizeof(path), "%s/data/filter/filter_0.yml", dirPath); config_t *cfg = cfgCreateDefault(); assert_non_null(cfg); filter_status_t res = cfgFilterStatus("redis", "redis", testAccessFilterPath(path), cfg); @@ -2791,7 +2791,7 @@ filterDenyIsProcessedAfterAllow(void **state) // redis is in both the allow list and deny list. // verify that "deny" wins. (is processed after allow) char path[PATH_MAX] = {0}; - scope_snprintf(path, sizeof(path), "%s/data/filter_4.yml", dirPath); + scope_snprintf(path, sizeof(path), "%s/data/filter/filter_4.yml", dirPath); config_t *cfg = cfgCreateDefault(); assert_non_null(cfg); filter_status_t res = cfgFilterStatus("redis", "", testAccessFilterPath(path), cfg); @@ -2807,7 +2807,7 @@ filterConfigIsProcessedAfterProcName(void **state) // cfg should be changed if procname or arg matches // make sure we process these fields before config. char path[PATH_MAX] = {0}; - scope_snprintf(path, sizeof(path), "%s/data/filter_4.yml", dirPath); + scope_snprintf(path, sizeof(path), "%s/data/filter/filter_4.yml", dirPath); config_t *cfg = cfgCreateDefault(); assert_non_null(cfg); @@ -2830,7 +2830,7 @@ filterMatchAllInAllow(void **state) { // Verify that _MatchAll_ in allow matches all processes char path[PATH_MAX] = {0}; - scope_snprintf(path, sizeof(path), "%s/data/filter_5.yml", dirPath); + scope_snprintf(path, sizeof(path), "%s/data/filter/filter_5.yml", dirPath); config_t *cfg = cfgCreateDefault(); assert_non_null(cfg); filter_status_t res = cfgFilterStatus("blue", "", testAccessFilterPath(path), cfg); @@ -2851,7 +2851,7 @@ filterMatchAllInAllowCanBeDenied(void **state) { // Verify that _MatchAll_ in allow is overriden by a match in deny char path[PATH_MAX] = {0}; - scope_snprintf(path, sizeof(path), "%s/data/filter_5.yml", dirPath); + scope_snprintf(path, sizeof(path), "%s/data/filter/filter_5.yml", dirPath); config_t *cfg = cfgCreateDefault(); assert_non_null(cfg); filter_status_t res = cfgFilterStatus("redis", "", testAccessFilterPath(path), cfg); @@ -2867,7 +2867,7 @@ filterVerifyMatchAllMergedConfig(void **state) // Verify that matches (including _MatchAll_) are applied in order // And that matches can be "merged" w.r.t. configuration char path[PATH_MAX] = {0}; - scope_snprintf(path, sizeof(path), "%s/data/filter_5.yml", dirPath); + scope_snprintf(path, sizeof(path), "%s/data/filter/filter_5.yml", dirPath); config_t *cfg = cfgCreateDefault(); assert_non_null(cfg); @@ -2898,7 +2898,7 @@ filterMatchAllInDeny(void **state) { // Verify that _MatchAll_ in deny denies all processes char path[PATH_MAX] = {0}; - scope_snprintf(path, sizeof(path), "%s/data/filter_6.yml", dirPath); + scope_snprintf(path, sizeof(path), "%s/data/filter/filter_6.yml", dirPath); config_t *cfg = cfgCreateDefault(); assert_non_null(cfg); filter_status_t res = cfgFilterStatus("blue", "", testAccessFilterPath(path), cfg); diff --git a/test/unit/library/ocitest.c b/test/unit/library/ocitest.c new file mode 100644 index 000000000..550c4b2b1 --- /dev/null +++ b/test/unit/library/ocitest.c @@ -0,0 +1,383 @@ +#define _GNU_SOURCE +#include +#include +#include +#include "cJSON.h" +#include "test.h" +#include "scopestdlib.h" + +#ifndef FALSE +#define FALSE 0 +#endif +#ifndef TRUE +#define TRUE 1 +#endif + +static char dirPath[PATH_MAX]; + +static int +testDirPath(char *path, const char *argv0) { + char buf[PATH_MAX]; + if (argv0[0] == '/') { + scope_strcpy(buf, argv0); + } else { + if (scope_getcwd(buf, PATH_MAX) == NULL) { + scope_perror("getcwd error"); + return -1; + } + scope_strcat(buf, "/"); + scope_strcat(buf, argv0); + } + + if (scope_realpath(buf, path) == NULL) { + scope_perror("scope_realpath error"); + return -1; + } + + /* + * Retrieve the test directory path. + * From: + * //appscope/test/linux/cfgutilsfiltertest + * To: + * //appscope/test/ + */ + for (int i= 0; i < 2; ++i) { + path = scope_dirname(path); + if (path == NULL) { + scope_perror("scope_dirname error"); + return -1; + } + } + return 0; +} + +// This function reflects the logic provided by rewriteOpenContainersConfig +static bool +rewriteOpenContainersConfigTest(int id) { + char inPath [PATH_MAX] = {0}; + char outPath [PATH_MAX] = {0}; + scope_snprintf(inPath, PATH_MAX, "%s/data/oci/oci%din.json", dirPath, id); + scope_snprintf(outPath, PATH_MAX, "%s/data/oci/oci%dout.json", dirPath, id); + + bool res = FALSE; + struct stat fileStat; + if (scope_stat(inPath, &fileStat) == -1) { + assert_non_null(NULL); + return res; + } + + FILE *fp = scope_fopen(inPath, "r"); + if (!fp) { + assert_non_null(NULL); + return res; + } + + /* + * Read the file contents into a string + */ + char *buf = (char *)scope_malloc(fileStat.st_size); + if (!buf) { + scope_fclose(fp); + assert_non_null(NULL); + return res; + } + + scope_fread(buf, sizeof(char), fileStat.st_size, fp); + scope_fclose(fp); + + + cJSON *json = cJSON_Parse(buf); + if (!json) { + assert_non_null(NULL); + return res; + } + scope_free(buf); + char *jsonStr = NULL; + + // Handle the process + cJSON *procNode = cJSON_GetObjectItemCaseSensitive(json, "process"); + if (!procNode) { + procNode = cJSON_CreateObject(); + if (!procNode) { + assert_non_null(NULL); + goto exit; + } + cJSON_AddItemToObject(json, "process", procNode); + } + cJSON *envNodeArr = cJSON_GetObjectItemCaseSensitive(procNode, "env"); + if (envNodeArr) { + bool ldPreloadPresent = FALSE; + // Iterate over environment string array + size_t envSize = cJSON_GetArraySize(envNodeArr); + for (int i = 0; i < envSize ;++i) { + cJSON *item = cJSON_GetArrayItem(envNodeArr, i); + char *strItem = cJSON_GetStringValue(item); + + if (scope_strncmp("LD_PRELOAD=", strItem, sizeof("LD_PRELOAD=")-1) == 0) { + size_t itemLen = scope_strlen(strItem); + size_t newLdprelLen = itemLen + sizeof("/opt/libscope.so:") - 1; + char *newLdPreloadLib = scope_calloc(1, newLdprelLen); + if (!newLdPreloadLib) { + assert_non_null(NULL); + goto exit; + } + scope_strncpy(newLdPreloadLib, "LD_PRELOAD=/opt/libscope.so:", sizeof("LD_PRELOAD=/opt/libscope.so:") - 1); + scope_strcat(newLdPreloadLib, strItem + sizeof("LD_PRELOAD=") - 1); + cJSON *newLdPreloadLibObj = cJSON_CreateString(newLdPreloadLib); + if (!newLdPreloadLibObj) { + scope_free(newLdPreloadLib); + assert_non_null(NULL); + goto exit; + } + cJSON_ReplaceItemInArray(envNodeArr, i, newLdPreloadLibObj); + scope_free(newLdPreloadLib); + + cJSON *scopeEnvNode = cJSON_CreateString("SCOPE_SETUP_DONE=true"); + if (!scopeEnvNode) { + assert_non_null(NULL); + goto exit; + } + cJSON_AddItemToArray(envNodeArr, scopeEnvNode); + ldPreloadPresent = TRUE; + break; + } else if (scope_strncmp("SCOPE_SETUP_DONE=true", strItem, sizeof("SCOPE_SETUP_DONE=true")-1) == 0) { + // we are done here + res = TRUE; + goto exit; + } + } + + + // There was no LD_PRELOAD in environment variables + if (ldPreloadPresent == FALSE) { + const char *const envItems[2] = + { + "LD_PRELOAD=/opt/libscope.so", + "SCOPE_SETUP_DONE=true" + }; + for (int i = 0; i < 2 ;++i) { + cJSON *scopeEnvNode = cJSON_CreateString(envItems[i]); + if (!scopeEnvNode) { + assert_non_null(NULL); + goto exit; + } + cJSON_AddItemToArray(envNodeArr, scopeEnvNode); + } + } + } else { + const char * envItems[2] = + { + "LD_PRELOAD=/opt/libscope.so", + "SCOPE_SETUP_DONE=true" + }; + envNodeArr = cJSON_CreateStringArray(envItems, 2); + if (!envNodeArr) { + assert_non_null(NULL); + goto exit; + } + cJSON_AddItemToObject(procNode, "env", envNodeArr); + } + + cJSON *mountNodeArr = cJSON_GetObjectItemCaseSensitive(json, "mounts"); + if (!mountNodeArr) { + mountNodeArr = cJSON_CreateArray(); + if (!mountNodeArr) { + assert_non_null(NULL); + goto exit; + } + cJSON_AddItemToObject(json, "mounts", mountNodeArr); + } + + cJSON *mountNode = cJSON_CreateObject(); + if (!mountNode) { + assert_non_null(NULL); + goto exit; + } + + if (!cJSON_AddStringToObjLN(mountNode, "destination", "/opt/scope")) { + cJSON_Delete(mountNode); + assert_non_null(NULL); + goto exit; + } + + if (!cJSON_AddStringToObjLN(mountNode, "type", "bind")) { + cJSON_Delete(mountNode); + assert_non_null(NULL); + goto exit; + } + + if (!cJSON_AddStringToObjLN(mountNode, "source", "/tmp/appscope/dev/scope")) { + cJSON_Delete(mountNode); + assert_non_null(NULL); + goto exit; + } + + const char *optItems[2] = + { + "rbind", + "rprivate" + }; + + cJSON *optNodeArr = cJSON_CreateStringArray(optItems, 2); + if (!optNodeArr) { + cJSON_Delete(mountNode); + assert_non_null(NULL); + goto exit; + } + cJSON_AddItemToObject(mountNode, "options", optNodeArr); + cJSON_AddItemToArray(mountNodeArr, mountNode); + + cJSON *hooksNode = cJSON_GetObjectItemCaseSensitive(json, "hooks"); + if (!hooksNode) { + hooksNode = cJSON_CreateObject(); + if (!hooksNode) { + assert_non_null(NULL); + goto exit; + } + cJSON_AddItemToObject(json, "hooks", hooksNode); + } + + cJSON *startContainerNodeArr = cJSON_GetObjectItemCaseSensitive(hooksNode, "startContainer"); + if (!startContainerNodeArr) { + startContainerNodeArr = cJSON_CreateArray(); + if (!startContainerNodeArr) { + assert_non_null(NULL); + goto exit; + } + cJSON_AddItemToObject(hooksNode, "startContainer", startContainerNodeArr); + } + + cJSON *startContainerNode = cJSON_CreateObject(); + if (!startContainerNode) { + assert_non_null(NULL); + goto exit; + } + + if (!cJSON_AddStringToObjLN(startContainerNode, "path", "/opt/scope")) { + cJSON_Delete(startContainerNode); + assert_non_null(NULL); + goto exit; + } + + const char *argsItems[3] = + { + "/opt/scope", + "extract", + "/opt" + }; + cJSON *argsNodeArr = cJSON_CreateStringArray(argsItems, 3); + if (!argsNodeArr) { + cJSON_Delete(startContainerNode); + assert_non_null(NULL); + goto exit; + } + cJSON_AddItemToObject(startContainerNode, "args", argsNodeArr); + cJSON_AddItemToArray(startContainerNodeArr, startContainerNode); + + jsonStr = cJSON_Print(json); + + // Overwrite the file + int fdOut = scope_open(outPath, O_RDONLY); + if (fdOut == -1) { + cJSON_free(jsonStr); + goto exit; + } + + struct stat stOut; + if (scope_fstat(fdOut, &stOut) == -1) { + cJSON_free(jsonStr); + scope_close(fdOut); + goto exit; + } + + + void *fdOutMap = scope_mmap(NULL, stOut.st_size, PROT_READ, MAP_PRIVATE, fdOut, 0); + if (fdOutMap == MAP_FAILED) { + cJSON_free(jsonStr); + scope_close(fdOut); + goto exit; + } + + if (memcmp(fdOutMap, jsonStr, stOut.st_size) != 0) { + assert_non_null(NULL); + } + scope_munmap(fdOutMap, stOut.st_size); + + cJSON_free(jsonStr); + scope_close(fdOut); + + res = TRUE; +exit: + cJSON_Delete(json); + return res; +} + +static void +ocitest_empty_json(void **state) +{ + bool res = rewriteOpenContainersConfigTest(0); + assert_int_equal(res, TRUE); +} + +static void +ocitest_process_only_json(void **state) +{ + bool res = rewriteOpenContainersConfigTest(1); + assert_int_equal(res, TRUE); +} + +static void +ocitest_process_env_present_preload(void **state) +{ + bool res = rewriteOpenContainersConfigTest(2); + assert_int_equal(res, TRUE); +} + +static void +ocitest_process_env_empty_preload(void **state) +{ + bool res = rewriteOpenContainersConfigTest(3); + assert_int_equal(res, TRUE); +} + +static void +ocitest_missing_hooks(void **state) +{ + bool res = rewriteOpenContainersConfigTest(4); + assert_int_equal(res, TRUE); +} + +static void +ocitest_hooks_incomplete(void **state) +{ + bool res = rewriteOpenContainersConfigTest(5); + assert_int_equal(res, TRUE); +} + +static void +ocitest_hooks_complete(void **state) +{ + bool res = rewriteOpenContainersConfigTest(6); + assert_int_equal(res, TRUE); +} + +int +main(int argc, char* argv[]) +{ + printf("running %s\n", argv[0]); + if (testDirPath(dirPath, argv[0])) { + return EXIT_FAILURE; + } + + const struct CMUnitTest tests[] = { + cmocka_unit_test(ocitest_empty_json), + cmocka_unit_test(ocitest_process_only_json), + cmocka_unit_test(ocitest_process_env_present_preload), + cmocka_unit_test(ocitest_process_env_empty_preload), + cmocka_unit_test(ocitest_missing_hooks), + cmocka_unit_test(ocitest_hooks_incomplete), + cmocka_unit_test(ocitest_hooks_complete), + cmocka_unit_test(dbgHasNoUnexpectedFailures), + }; + return cmocka_run_group_tests(tests, groupSetup, groupTeardown); +} diff --git a/test/unit/library/utilstest.c b/test/unit/library/utilstest.c index 93741abfe..19d80a63d 100644 --- a/test/unit/library/utilstest.c +++ b/test/unit/library/utilstest.c @@ -2,10 +2,11 @@ #include #include #include +#include #include + #include "utils.h" #include "scopestdlib.h" - #include "fn.h" #include "test.h"