From 31b88da8431096a6df276705046ca7a012fa3530 Mon Sep 17 00:00:00 2001 From: Brigid Smith Date: Wed, 23 Jul 2014 11:22:25 -0700 Subject: [PATCH 1/2] Added test for ifunc support in dynamic linker. ifuncs now work in i386 and x86_64 when called in the same library as well as in a different library. Bug:6657325 (cherry picked from commit c5a13efa9bc4264be0a9a9e37c00633af01584ed) Change-Id: I321d780bc2f9bd1baa749e1acacd2683aefe827b --- libc/include/elf.h | 19 ++--- linker/linker.cpp | 114 +++++++++++++++++++++++++++++- linker/linker.h | 6 ++ linker/linker_debug.h | 1 + tests/dlfcn_test.cpp | 33 +++++++++ tests/libs/Android.mk | 14 ++++ tests/libs/dlopen_testlib_ifunc.c | 37 ++++++++++ 7 files changed, 214 insertions(+), 10 deletions(-) create mode 100644 tests/libs/dlopen_testlib_ifunc.c diff --git a/libc/include/elf.h b/libc/include/elf.h index 0975b7a61..faae73e92 100644 --- a/libc/include/elf.h +++ b/libc/include/elf.h @@ -69,14 +69,17 @@ typedef struct { #define PT_GNU_RELRO 0x6474e552 -#define STB_LOOS 10 -#define STB_HIOS 12 -#define STB_LOPROC 13 -#define STB_HIPROC 15 +#define STB_LOOS 10 +#define STB_HIOS 12 +#define STB_LOPROC 13 +#define STB_HIPROC 15 -#define STT_LOOS 10 -#define STT_HIOS 12 -#define STT_LOPROC 13 -#define STT_HIPROC 15 +#define STT_GNU_IFUNC 10 +#define STT_LOOS 10 +#define STT_HIOS 12 +#define STT_LOPROC 13 +#define STT_HIPROC 15 + +#define R_386_IRELATIVE 42 #endif /* _ELF_H */ diff --git a/linker/linker.cpp b/linker/linker.cpp index cf657057e..b28a01d49 100644 --- a/linker/linker.cpp +++ b/linker/linker.cpp @@ -466,6 +466,29 @@ static ElfW(Sym)* soinfo_elf_lookup(soinfo* si, unsigned hash, const char* name) return NULL; } +static void resolve_ifunc_symbols(soinfo* si) { + + phdr_table_unprotect_segments(si->phdr, si->phnum, si->load_bias); + + TRACE_TYPE(IFUNC, "CHECKING FOR IFUNCS AND PERFORMING SYMBOL UPDATES"); + + for (size_t i = 0; i < si->nchain; ++i) { + ElfW(Sym)* s = &si->symtab[i]; + if (ELF_ST_TYPE(s->st_info) == STT_GNU_IFUNC) { + // The address of the ifunc in the symbol table is the address of the + // function that chooses the function to which the ifunc will refer. + // In order to return the proper value, we run the choosing function + // in the linker and then return its result (minus the base offset). + TRACE_TYPE(IFUNC, "FOUND IFUNC"); + ElfW(Addr) (*ifunc_ptr)(); + ifunc_ptr = reinterpret_cast(s->st_value + si->base); + s->st_value = (ifunc_ptr() - si->base); + TRACE_TYPE(IFUNC, "NEW VALUE IS %p", (void*)s->st_value); + } + } + phdr_table_protect_segments(si->phdr, si->phnum, si->load_bias); +} + static unsigned elfhash(const char* _name) { const unsigned char* name = reinterpret_cast(_name); unsigned h = 0, g; @@ -804,6 +827,10 @@ static soinfo* load_library(const char* name, int dlflags, const android_dlextin return NULL; } + // if the library has any ifuncs, we will need to resolve them so that dlsym + // can handle them properly + resolve_ifunc_symbols(si); + return si; } @@ -931,6 +958,53 @@ void do_dlclose(soinfo* si) { protect_data(PROT_READ); } +// ifuncs are only defined for x86 +#if defined(__i386__) +static void soinfo_ifunc_relocate(soinfo* si, ElfW(Rel)* rel, unsigned count, soinfo* needed[]) { + for (size_t idx = 0; idx < count; ++idx, ++rel) { + ElfW(Sym)* s; + soinfo* lsi; + unsigned type = ELFW(R_TYPE)(rel->r_info); + unsigned sym = ELFW(R_SYM)(rel->r_info); + ElfW(Addr) reloc = static_cast(rel->r_offset + si->load_bias); + ElfW(Addr) sym_addr = 0; + const char* sym_name = NULL; + sym_name = reinterpret_cast(si->strtab + si->symtab[sym].st_name); + s = soinfo_do_lookup(si, sym_name, &lsi, needed); + + if (ELF_ST_TYPE(s->st_info) == STT_GNU_IFUNC && type == R_386_JMP_SLOT) { + TRACE("IFUNC RELOCATION, PASS 2: %p", (void*)(sym_addr)); + ElfW(Addr) (*ifunc_ptr)(); + ifunc_ptr = reinterpret_cast(s->st_value + si->base); + *reinterpret_cast(reloc) = ifunc_ptr(); + } + } +} +#endif + +#if defined(__x86_64__) +static void soinfo_ifunc_relocate(soinfo* si, ElfW(Rela)* rela, unsigned count, soinfo* needed[]) { + for (size_t idx = 0; idx < count; ++idx, ++rela) { + ElfW(Sym)* s; + soinfo* lsi; + unsigned type = ELFW(R_TYPE)(rela->r_info); + unsigned sym = ELFW(R_SYM)(rela->r_info); + ElfW(Addr) reloc = static_cast(rela->r_offset + si->load_bias); + ElfW(Addr) sym_addr = 0; + const char* sym_name = NULL; + sym_name = reinterpret_cast(si->strtab + si->symtab[sym].st_name); + s = soinfo_do_lookup(si, sym_name, &lsi, needed); + + if (ELF_ST_TYPE(s->st_info) == STT_GNU_IFUNC && type == R_X86_64_JUMP_SLOT) { + TRACE("IFUNC RELOCATION, PASS 2: %p", (void*)(sym_addr + rela->r_addend)); + ElfW(Addr) (*ifunc_ptr)(); + ifunc_ptr = reinterpret_cast(s->st_value + si->base); + *reinterpret_cast(reloc) = ifunc_ptr(); + } + } +} +#endif + #if defined(USE_RELA) static int soinfo_relocate(soinfo* si, ElfW(Rela)* rela, unsigned count, soinfo* needed[]) { ElfW(Sym)* s; @@ -1142,7 +1216,11 @@ static int soinfo_relocate(soinfo* si, ElfW(Rela)* rela, unsigned count, soinfo* MARK(rela->r_offset); TRACE_TYPE(RELO, "RELO JMP_SLOT %08zx <- %08zx %s", static_cast(reloc), static_cast(sym_addr + rela->r_addend), sym_name); - *reinterpret_cast(reloc) = sym_addr + rela->r_addend; + if (ELF_ST_TYPE(s->st_info) == STT_GNU_IFUNC) { + si->set_has_ifuncs(true); + } else { + *reinterpret_cast(reloc) = sym_addr + rela->r_addend; + } break; case R_X86_64_GLOB_DAT: count_relocation(kRelocAbsolute); @@ -1321,7 +1399,11 @@ static int soinfo_relocate(soinfo* si, ElfW(Rel)* rel, unsigned count, soinfo* n count_relocation(kRelocAbsolute); MARK(rel->r_offset); TRACE_TYPE(RELO, "RELO JMP_SLOT %08x <- %08x %s", reloc, sym_addr, sym_name); - *reinterpret_cast(reloc) = sym_addr; + if (ELF_ST_TYPE(s->st_info) == STT_GNU_IFUNC) { + si->set_has_ifuncs(true); + } else { + *reinterpret_cast(reloc) = sym_addr; + } break; case R_386_GLOB_DAT: count_relocation(kRelocAbsolute); @@ -1581,6 +1663,14 @@ void soinfo::set_st_ino(ino_t ino) { st_ino = ino; } +void soinfo::set_has_ifuncs(bool ifuncs) { + if ((this->flags & FLAG_NEW_SOINFO) == 0) { + return; + } + + has_ifuncs = ifuncs; +} + dev_t soinfo::get_st_dev() { if ((this->flags & FLAG_NEW_SOINFO) == 0) { return 0; @@ -1597,6 +1687,14 @@ ino_t soinfo::get_st_ino() { return st_ino; } +bool soinfo::get_has_ifuncs() { + if ((this->flags & FLAG_NEW_SOINFO) == 0) { + return false; + } + + return has_ifuncs; +} + // This is a return on get_children() in case // 'this->flags' does not have FLAG_NEW_SOINFO set. static soinfo::soinfo_list_t g_empty_list; @@ -1981,6 +2079,18 @@ static bool soinfo_link_image(soinfo* si, const android_dlextinfo* extinfo) { } #endif + // if there are ifuncs, we need to do an additional relocation pass. + // they cannot be resolved until the rest of the relocations are done + // because we need to call the resolution function which may be waiting + // on relocations. + if(si->get_has_ifuncs()) { +#if defined(__i386__) + soinfo_ifunc_relocate(si, si->plt_rel, si->plt_rel_count, needed); +#elif defined(__x86_64__) + soinfo_ifunc_relocate(si, si->plt_rela, si->plt_rela_count, needed); +#endif + } + #if defined(__mips__) if (!mips_relocate_got(si, needed)) { return false; diff --git a/linker/linker.h b/linker/linker.h index 374652eb8..d7cf24bfa 100644 --- a/linker/linker.h +++ b/linker/linker.h @@ -204,8 +204,12 @@ struct soinfo { void set_st_dev(dev_t st_dev); void set_st_ino(ino_t st_ino); + void set_has_ifuncs(bool ifunc); ino_t get_st_ino(); dev_t get_st_dev(); + bool get_has_ifuncs(); + + soinfo_list_t& get_children(); @@ -218,6 +222,8 @@ struct soinfo { // when FLAG_NEW_SOINFO is set in this->flags. unsigned int version; + bool has_ifuncs; + dev_t st_dev; ino_t st_ino; diff --git a/linker/linker_debug.h b/linker/linker_debug.h index 3faa38ef4..0c7a78418 100644 --- a/linker/linker_debug.h +++ b/linker/linker_debug.h @@ -42,6 +42,7 @@ #define TRACE_DEBUG 1 #define DO_TRACE_LOOKUP 1 #define DO_TRACE_RELO 1 +#define DO_TRACE_IFUNC 1 #define TIMING 0 #define STATS 0 #define COUNT_PAGES 0 diff --git a/tests/dlfcn_test.cpp b/tests/dlfcn_test.cpp index 457fcd5c0..260cbd601 100644 --- a/tests/dlfcn_test.cpp +++ b/tests/dlfcn_test.cpp @@ -87,6 +87,39 @@ TEST(dlfcn, dlopen_noload) { ASSERT_EQ(0, dlclose(handle2)); } +// ifuncs are only supported on intel for now +#if defined(__i386__) || defined(__x86_64__) +TEST(dlfcn, ifunc) { + const char* (*foo_ptr)(); + const char* (*foo_library_ptr)(); + + // ifunc's choice depends on whether IFUNC_CHOICE has a value + // first check the set case + setenv("IFUNC_CHOICE", "set", 1); + void* handle = dlopen("libtest_ifunc.so", RTLD_NOW); + ASSERT_TRUE(handle != NULL); + *(void **)(&foo_ptr) = dlsym(handle, "foo"); + *(void **)(&foo_library_ptr) = dlsym(handle, "foo_library"); + ASSERT_TRUE(foo_ptr != NULL); + ASSERT_TRUE(foo_library_ptr != NULL); + ASSERT_EQ(strncmp("set", (*foo_ptr)(), 3), 0); + ASSERT_EQ(strncmp("set", (*foo_library_ptr)(), 3), 0); + dlclose(handle); + + // then check the unset case + unsetenv("IFUNC_CHOICE"); + handle = dlopen("libtest_ifunc.so", RTLD_NOW); + ASSERT_TRUE(handle != NULL); + *(void **)(&foo_ptr) = dlsym(handle, "foo"); + *(void **)(&foo_library_ptr) = dlsym(handle, "foo_library"); + ASSERT_TRUE(foo_ptr != NULL); + ASSERT_TRUE(foo_library_ptr != NULL); + ASSERT_EQ(strncmp("unset", (*foo_ptr)(), 5), 0); + ASSERT_EQ(strncmp("unset", (*foo_library_ptr)(), 3), 0); + dlclose(handle); +} +#endif + TEST(dlfcn, dlopen_failure) { void* self = dlopen("/does/not/exist", RTLD_NOW); ASSERT_TRUE(self == NULL); diff --git a/tests/libs/Android.mk b/tests/libs/Android.mk index 75df539b9..8f0ec7a56 100644 --- a/tests/libs/Android.mk +++ b/tests/libs/Android.mk @@ -114,6 +114,20 @@ build_type := target build_target := SHARED_LIBRARY include $(TEST_PATH)/Android.build.mk +# ----------------------------------------------------------------------------- +# Library used by ifunc tests +# ----------------------------------------------------------------------------- +ifeq ($(TARGET_ARCH),$(filter $(TARGET_ARCH),x86 x86_64)) + libtest_ifunc_src_files := \ + dlopen_testlib_ifunc.c + + LOCAL_SDK_VERSION := current + module := libtest_ifunc + build_type := target + build_target := SHARED_LIBRARY + include $(TEST_PATH)/Android.build.mk +endif + # ----------------------------------------------------------------------------- # Library used by atexit tests # ----------------------------------------------------------------------------- diff --git a/tests/libs/dlopen_testlib_ifunc.c b/tests/libs/dlopen_testlib_ifunc.c new file mode 100644 index 000000000..1c4baface --- /dev/null +++ b/tests/libs/dlopen_testlib_ifunc.c @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +const char* foo() __attribute__ ((ifunc ("foo_ifunc"))); + +const char* f1() { + return "unset"; +} + +const char* f2() { + return "set"; +} + +void* foo_ifunc() { + char* choice = getenv("IFUNC_CHOICE"); + return choice == NULL ? f1 : f2; +} + +const char* foo_library() { + return foo(); +} \ No newline at end of file From bd321c1106ed30a71d55d5c365335dfe552b0883 Mon Sep 17 00:00:00 2001 From: Dmitriy Ivanov Date: Thu, 21 Aug 2014 13:54:03 -0700 Subject: [PATCH 2/2] Run constructors before resolving ifunc functions Bug: 17177284 (cherry picked from commit 9598b8c415e2fa9f240508185fe8c964b83f538d) Change-Id: I2c9631ee1cd77f8cf95ec0216a35b605c8786454 --- libc/include/elf.h | 2 -- linker/linker.cpp | 23 ++++++++++++----------- linker/linker.h | 1 + tests/Android.mk | 16 ++++++++++++++++ tests/dlfcn_test.cpp | 30 ++++++++++++++++++++---------- tests/libs/dlopen_testlib_ifunc.c | 21 ++++++++++++++++++++- 6 files changed, 69 insertions(+), 24 deletions(-) diff --git a/libc/include/elf.h b/libc/include/elf.h index faae73e92..7a9485aea 100644 --- a/libc/include/elf.h +++ b/libc/include/elf.h @@ -80,6 +80,4 @@ typedef struct { #define STT_LOPROC 13 #define STT_HIPROC 15 -#define R_386_IRELATIVE 42 - #endif /* _ELF_H */ diff --git a/linker/linker.cpp b/linker/linker.cpp index b28a01d49..ec0681fe6 100644 --- a/linker/linker.cpp +++ b/linker/linker.cpp @@ -466,14 +466,17 @@ static ElfW(Sym)* soinfo_elf_lookup(soinfo* si, unsigned hash, const char* name) return NULL; } -static void resolve_ifunc_symbols(soinfo* si) { +void soinfo::resolve_ifunc_symbols() { + if (!get_has_ifuncs()) { + return; + } - phdr_table_unprotect_segments(si->phdr, si->phnum, si->load_bias); + phdr_table_unprotect_segments(phdr, phnum, load_bias); TRACE_TYPE(IFUNC, "CHECKING FOR IFUNCS AND PERFORMING SYMBOL UPDATES"); - for (size_t i = 0; i < si->nchain; ++i) { - ElfW(Sym)* s = &si->symtab[i]; + for (size_t i = 0; i < nchain; ++i) { + ElfW(Sym)* s = &symtab[i]; if (ELF_ST_TYPE(s->st_info) == STT_GNU_IFUNC) { // The address of the ifunc in the symbol table is the address of the // function that chooses the function to which the ifunc will refer. @@ -481,12 +484,12 @@ static void resolve_ifunc_symbols(soinfo* si) { // in the linker and then return its result (minus the base offset). TRACE_TYPE(IFUNC, "FOUND IFUNC"); ElfW(Addr) (*ifunc_ptr)(); - ifunc_ptr = reinterpret_cast(s->st_value + si->base); - s->st_value = (ifunc_ptr() - si->base); + ifunc_ptr = reinterpret_cast(s->st_value + base); + s->st_value = (ifunc_ptr() - base); TRACE_TYPE(IFUNC, "NEW VALUE IS %p", (void*)s->st_value); } } - phdr_table_protect_segments(si->phdr, si->phnum, si->load_bias); + phdr_table_protect_segments(phdr, phnum, load_bias); } static unsigned elfhash(const char* _name) { @@ -827,10 +830,6 @@ static soinfo* load_library(const char* name, int dlflags, const android_dlextin return NULL; } - // if the library has any ifuncs, we will need to resolve them so that dlsym - // can handle them properly - resolve_ifunc_symbols(si); - return si; } @@ -1599,6 +1598,8 @@ void soinfo::CallConstructors() { // DT_INIT should be called before DT_INIT_ARRAY if both are present. CallFunction("DT_INIT", init_func); CallArray("DT_INIT_ARRAY", init_array, init_array_count, false); + + resolve_ifunc_symbols(); } void soinfo::CallDestructors() { diff --git a/linker/linker.h b/linker/linker.h index d7cf24bfa..684561a88 100644 --- a/linker/linker.h +++ b/linker/linker.h @@ -216,6 +216,7 @@ struct soinfo { private: void CallArray(const char* array_name, linker_function_t* functions, size_t count, bool reverse); void CallFunction(const char* function_name, linker_function_t function); + void resolve_ifunc_symbols(); private: // This part of the structure is only available diff --git a/tests/Android.mk b/tests/Android.mk index 8184bf74b..9ee33e004 100644 --- a/tests/Android.mk +++ b/tests/Android.mk @@ -356,6 +356,22 @@ bionic-unit-tests-run-on-host: bionic-unit-tests $(TARGET_OUT_EXECUTABLES)/$(LIN $(TARGET_OUT_DATA_NATIVE_TESTS)/bionic-unit-tests/bionic-unit-tests$(NATIVE_TEST_SUFFIX) $(BIONIC_TEST_FLAGS) endif +ifeq ($(TARGET_ARCH),$(filter $(TARGET_ARCH),x86_64)) +# add target to run lp32 tests +bionic-unit-tests-run-on-host32: bionic-unit-tests $(TARGET_OUT_EXECUTABLES)/$(LINKER) $(TARGET_OUT_EXECUTABLES)/sh + if [ ! -d /system -o ! -d /system/bin ]; then \ + echo "Attempting to create /system/bin"; \ + sudo mkdir -p -m 0777 /system/bin; \ + fi + mkdir -p $(TARGET_OUT_DATA)/local/tmp + cp $(TARGET_OUT_EXECUTABLES)/linker /system/bin + cp $(TARGET_OUT_EXECUTABLES)/sh /system/bin + ANDROID_DATA=$(TARGET_OUT_DATA) \ + ANDROID_ROOT=$(TARGET_OUT) \ + LD_LIBRARY_PATH=$(2ND_TARGET_OUT_SHARED_LIBRARIES) \ + $(2ND_TARGET_OUT_DATA_NATIVE_TESTS)/bionic-unit-tests/bionic-unit-tests32 $(BIONIC_TEST_FLAGS) +endif + endif # linux-x86 include $(call first-makefiles-under,$(LOCAL_PATH)) diff --git a/tests/dlfcn_test.cpp b/tests/dlfcn_test.cpp index 260cbd601..6de38c89e 100644 --- a/tests/dlfcn_test.cpp +++ b/tests/dlfcn_test.cpp @@ -90,32 +90,42 @@ TEST(dlfcn, dlopen_noload) { // ifuncs are only supported on intel for now #if defined(__i386__) || defined(__x86_64__) TEST(dlfcn, ifunc) { - const char* (*foo_ptr)(); - const char* (*foo_library_ptr)(); + typedef const char* (*fn_ptr)(); // ifunc's choice depends on whether IFUNC_CHOICE has a value // first check the set case setenv("IFUNC_CHOICE", "set", 1); void* handle = dlopen("libtest_ifunc.so", RTLD_NOW); ASSERT_TRUE(handle != NULL); - *(void **)(&foo_ptr) = dlsym(handle, "foo"); - *(void **)(&foo_library_ptr) = dlsym(handle, "foo_library"); + fn_ptr foo_ptr = reinterpret_cast(dlsym(handle, "foo")); + fn_ptr foo_library_ptr = reinterpret_cast(dlsym(handle, "foo_library")); ASSERT_TRUE(foo_ptr != NULL); ASSERT_TRUE(foo_library_ptr != NULL); - ASSERT_EQ(strncmp("set", (*foo_ptr)(), 3), 0); - ASSERT_EQ(strncmp("set", (*foo_library_ptr)(), 3), 0); + ASSERT_EQ(strncmp("set", foo_ptr(), 3), 0); + ASSERT_EQ(strncmp("set", foo_library_ptr(), 3), 0); dlclose(handle); // then check the unset case unsetenv("IFUNC_CHOICE"); handle = dlopen("libtest_ifunc.so", RTLD_NOW); ASSERT_TRUE(handle != NULL); - *(void **)(&foo_ptr) = dlsym(handle, "foo"); - *(void **)(&foo_library_ptr) = dlsym(handle, "foo_library"); + foo_ptr = reinterpret_cast(dlsym(handle, "foo")); + foo_library_ptr = reinterpret_cast(dlsym(handle, "foo_library")); ASSERT_TRUE(foo_ptr != NULL); ASSERT_TRUE(foo_library_ptr != NULL); - ASSERT_EQ(strncmp("unset", (*foo_ptr)(), 5), 0); - ASSERT_EQ(strncmp("unset", (*foo_library_ptr)(), 3), 0); + ASSERT_EQ(strncmp("unset", foo_ptr(), 5), 0); + ASSERT_EQ(strncmp("unset", foo_library_ptr(), 3), 0); + dlclose(handle); +} + +TEST(dlfcn, ifunc_ctor_call) { + typedef const char* (*fn_ptr)(); + + void* handle = dlopen("libtest_ifunc.so", RTLD_NOW); + ASSERT_TRUE(handle != NULL) << dlerror(); + fn_ptr is_ctor_called = reinterpret_cast(dlsym(handle, "is_ctor_called")); + ASSERT_TRUE(is_ctor_called != NULL) << dlerror(); + ASSERT_STREQ("true", is_ctor_called()); dlclose(handle); } #endif diff --git a/tests/libs/dlopen_testlib_ifunc.c b/tests/libs/dlopen_testlib_ifunc.c index 1c4baface..48748417d 100644 --- a/tests/libs/dlopen_testlib_ifunc.c +++ b/tests/libs/dlopen_testlib_ifunc.c @@ -17,7 +17,22 @@ #include #include +static int g_flag = 0; + +static void __attribute__((constructor)) init_flag() { + g_flag = 1; +} + const char* foo() __attribute__ ((ifunc ("foo_ifunc"))); +const char* is_ctor_called() __attribute__ ((ifunc("is_ctor_called_ifun"))); + +const char* return_true() { + return "true"; +} + +const char* return_false() { + return "false"; +} const char* f1() { return "unset"; @@ -27,6 +42,10 @@ const char* f2() { return "set"; } +void* is_ctor_called_ifun() { + return g_flag == 0 ? return_false : return_true; +} + void* foo_ifunc() { char* choice = getenv("IFUNC_CHOICE"); return choice == NULL ? f1 : f2; @@ -34,4 +53,4 @@ void* foo_ifunc() { const char* foo_library() { return foo(); -} \ No newline at end of file +}