Make it possible to run elf/so files directly

This commit is contained in:
Ken Van Hoeylandt 2026-09-03 22:30:52 +02:00
parent 643cbc3806
commit 92e5c9bf43
15 changed files with 781 additions and 15 deletions

View File

@ -3,6 +3,8 @@
#include <sdkconfig.h>
#endif
#include <app/elf_check.h>
#include <app/exec.h>
#include <app/loader.h>
#include <app/location.h>
@ -72,6 +74,28 @@ std::string resolve_elf_path(const std::string& path) {
return path + "/elf/" + CONFIG_IDF_TARGET + ".elf";
}
constexpr ElfRequirements EXECUTABLE_REQUIREMENTS = {
.elf_class = ELF_CLASS_32,
.data = ELF_DATA_2LSB,
.type = ELF_TYPE_DYN,
#if defined(__XTENSA__)
.machine = ELF_MACHINE_XTENSA,
#elif defined(__riscv)
.machine = ELF_MACHINE_RISCV,
#else
#error "Unsupported ESP32 architecture for ELF machine check"
#endif
};
// Validates an already-resolved binary path (see resolve_elf_path()) before it's handed to
// esp_elf_relocate(), which performs no header validation of its own: the extension and
// directory checks are cheap string comparisons, so the file is only opened as a last resort.
bool is_executable_file(const std::string& resolved_path) {
return resolved_path.ends_with(".elf")
&& app_exec_path_allowed(resolved_path.c_str())
&& elf_check_file(resolved_path.c_str(), &EXECUTABLE_REQUIREMENTS);
}
error_t api_load(AppLocation location, AppRuntime* out_runtime) {
if (location.type != APP_LOCATION_PATH) {
LOG_E(TAG, "Out of memory");
@ -88,6 +112,12 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) {
auto elf_path = resolve_elf_path(static_cast<const char*>(location.location));
if (!is_executable_file(elf_path)) {
LOG_E(TAG, "Not executable: %s", elf_path.c_str());
delete runtime;
return ERROR_NOT_ALLOWED;
}
size_t size = 0;
error_t read_result = read_file(elf_path.c_str(), &runtime->file_data, &size);
if (read_result != ERROR_NONE) {
@ -127,10 +157,20 @@ void api_unload(AppRuntime runtime_ptr) {
delete runtime;
}
bool api_is_executable(AppLocation location) {
if (location.type != APP_LOCATION_PATH) {
return false;
}
auto elf_path = resolve_elf_path(static_cast<const char*>(location.location));
return is_executable_file(elf_path);
}
AppLoaderApi loader_api = {
.load = api_load,
.run = api_run,
.unload = api_unload,
.is_executable = api_is_executable,
};
void* create_service(const ServiceManifest*) {

View File

@ -0,0 +1,38 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// <elf.h> is not guaranteed to exist under the ESP32 newlib toolchain
#define ELF_CLASS_32 1
#define ELF_CLASS_64 2
#define ELF_DATA_2LSB 1
#define ELF_TYPE_DYN 3
#define ELF_MACHINE_XTENSA 94
#define ELF_MACHINE_RISCV 243
#define ELF_MACHINE_X86_64 62
#define ELF_MACHINE_AARCH64 183
/** What a loader needs an ELF file's header to say before it will try to load it. */
struct ElfRequirements {
uint8_t elf_class; /**< ELF_CLASS_32 / ELF_CLASS_64 */
uint8_t data; /**< ELF_DATA_2LSB */
uint16_t type; /**< ELF_TYPE_DYN */
uint16_t machine; /**< ELF_MACHINE_XTENSA / _RISCV / _X86_64 / _AARCH64 */
};
/**
* Reads the first 20 bytes of @a path (e_ident, e_type, e_machine; identical offsets for
* ELF32 and ELF64) and checks the magic number and every field in @a requirements match.
* @return false if @a path can't be opened, is too short, or doesn't match
*/
bool elf_check_file(const char* path, const struct ElfRequirements* requirements);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,57 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/instance.h>
#include <stdbool.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
/** Largest number of directories app_exec_path_add() can hold at once. */
#define APP_EXEC_MAX_PATHS 8
/**
* Registers @a path as a directory whose contents may be executed. Applies recursively to
* subdirectories. Registering the same path twice is a no-op.
* @retval ERROR_OUT_OF_MEMORY the registry already holds APP_EXEC_MAX_PATHS entries
* @retval ERROR_NONE on success (including when @a path was already registered)
*/
error_t app_exec_path_add(const char* path);
/**
* Unregisters a directory previously passed to app_exec_path_add().
* @retval ERROR_NOT_FOUND @a path was not registered
* @retval ERROR_NONE on success
*/
error_t app_exec_path_remove(const char* path);
/**
* True when @a file_path lives under a directory registered with app_exec_path_add(), at any
* depth. Rejects any path containing a ".." segment rather than normalising it.
*/
bool app_exec_path_allowed(const char* file_path);
/**
* Asks the registered APP_LOADER_PATH_SERVICE_ID loader (see app/loader.h) whether @a path is
* runnable on this target, without loading it.
* @return false if @a path can't be run, or if no path loader is registered
*/
bool app_exec_is_executable_path(const char* path);
/**
* Runs @a path directly, without it having to be pre-registered as an app (e.g. via
* app_manager_add() or app_install()), for a file a user picked in a file browser. Internally
* registers (app_manager_add(), APP_MANIFEST_FLAG_HIDDEN so it never shows up in app-browsing
* UIs) a manifest for @a path the first time it's run, reusing that same registration on
* subsequent calls for the same path, then starts it (app_manager_start_with_parameters()).
* @retval ERROR_NOT_ALLOWED app_exec_is_executable_path(path) is false
* @retval ERROR_NONE on success
*/
error_t app_exec_run_path(const char* path, int argc, const char* const argv[], AppInstanceId* out_app_instance_id);
#ifdef __cplusplus
}
#endif

View File

@ -3,6 +3,7 @@
#include <app/manifest.h>
#include <tactility/error.h>
#include <stdbool.h>
#include <stdint.h>
#include "location.h"
@ -18,15 +19,8 @@ extern "C" {
* APP_LOCATION_PATH must register under. Implemented by a platform module (e.g. app-esp32-module). */
#define APP_LOADER_PATH_SERVICE_ID "app-loader-path"
/**
* Entry point signature for an APP_LOCATION_MEMORY app: a function linked directly into this
* firmware binary. Called on the dedicated task app-module's scheduler spawns for this instance,
* blocking for the app's whole lifetime - same contract as an external app's main(). Use
* app_scheduler_current_app_id() to identify this running instance (e.g. with
* app_event_subscribe()/window_manager_create()/etc.). The instance closes when this function
* returns - no separate call is needed.
* AppManifest::location.location holds this cast to void*.
*/
/** Entry point signature for an APP_LOCATION_MEMORY app.
* AppManifest::location.location holds this cast to void*. */
typedef int32_t (*AppMainFn)(int argc, char* argv[]);
typedef void* AppRuntime;
@ -53,6 +47,11 @@ struct AppLoaderApi {
/** Releases whatever load() allocated. Called after run() returns. */
void (*unload)(AppRuntime runtime);
/**
* Reports whether this loader could load and run whatever @a location points at, without actually loading it.
*/
bool (*is_executable)(struct AppLocation location);
};
#ifdef __cplusplus

View File

@ -0,0 +1,16 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/**
* Registers app-module's default executable directories (data/bin, system/bin, the app install
* root) with app_exec_path_add(). Called once from app-module's own Module::start().
*/
void app_exec_register_default_paths(void);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,45 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/elf_check.h>
#include <cstdio>
#include <cstring>
namespace {
constexpr size_t ELF_HEADER_PREFIX_SIZE = 20; // e_ident[16] + e_type(2) + e_machine(2)
constexpr uint8_t ELF_MAGIC[4] = { 0x7f, 'E', 'L', 'F' };
uint16_t read_le16(const uint8_t* bytes) {
return static_cast<uint16_t>(bytes[0] | (bytes[1] << 8));
}
} // namespace
bool elf_check_file(const char* path, const struct ElfRequirements* requirements) {
FILE* file = fopen(path, "rb");
if (file == nullptr) {
return false;
}
uint8_t header[ELF_HEADER_PREFIX_SIZE];
size_t read = fread(header, 1, sizeof(header), file);
fclose(file);
if (read != sizeof(header)) {
return false;
}
if (memcmp(header, ELF_MAGIC, sizeof(ELF_MAGIC)) != 0) {
return false;
}
uint8_t elf_class = header[4]; // e_ident[EI_CLASS]
uint8_t data = header[5]; // e_ident[EI_DATA]
uint16_t type = read_le16(header + 16); // e_type
uint16_t machine = read_le16(header + 18); // e_machine
return elf_class == requirements->elf_class
&& data == requirements->data
&& type == requirements->type
&& machine == requirements->machine;
}

View File

@ -0,0 +1,273 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/exec.h>
#include <app/private/exec_internal.h>
#include <app/loader.h>
#include <app/location.h>
#include <app/manager.h>
#include <app/manifest.h>
#include <service/instance.h>
#include <service/manager.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/filesystem/file_system.h>
#include <tactility/log.h>
#include <tactility/paths.h>
#include <climits>
#include <cstdio>
#include <cstring>
#include <memory>
#include <string>
#include <unistd.h>
#include <unordered_map>
constexpr auto* TAG = "app_exec";
namespace {
struct ExecPathRegistry {
std::string paths[APP_EXEC_MAX_PATHS];
size_t count = 0;
Mutex mutex {};
ExecPathRegistry() { mutex_construct(&mutex); }
};
ExecPathRegistry& registry() {
static ExecPathRegistry instance;
return instance;
}
// POSIX works with relative paths. The rest of the OS uses anchors relative paths.
// Resolving through getcwd() keeps every path callers pass in comparable, regardless of which convention they used.
// A path already starting with '/' passes through unchanged, so this is a no-op on ESP32.
std::string make_absolute(const std::string& path) {
if (path.empty() || path.front() == '/') {
return path;
}
char cwd[PATH_MAX];
if (getcwd(cwd, sizeof(cwd)) == nullptr) {
return path; // best effort: falls back to today's (broken but not worse) behavior
}
return std::string(cwd) + "/" + path;
}
// Resolves to absolute (see make_absolute()) and strips a trailing '/' so "/data/bin/" and
// "/data/bin" register/compare identically.
std::string normalize(const char* path) {
std::string result = make_absolute(path);
while (result.size() > 1 && result.back() == '/') {
result.pop_back();
}
return result;
}
bool has_dotdot_segment(const std::string& path) {
size_t start = 0;
while (start <= path.size()) {
size_t slash = path.find('/', start);
size_t length = (slash == std::string::npos ? path.size() : slash) - start;
if (path.compare(start, length, "..") == 0) {
return true;
}
if (slash == std::string::npos) {
break;
}
start = slash + 1;
}
return false;
}
// True when @a file_path lives strictly under @a dir (a direct or nested child). Never equal to
// @a dir itself: a bare directory is never itself an executable candidate.
bool is_under_directory(const std::string& file_path, const std::string& dir) {
if (file_path.size() <= dir.size() || file_path.compare(0, dir.size(), dir) != 0) {
return false;
}
return file_path[dir.size()] == '/';
}
const AppLoaderApi* find_path_loader_api() {
ServiceInstance* instance = service_manager_find_instance(APP_LOADER_PATH_SERVICE_ID);
if (instance == nullptr) {
return nullptr;
}
return static_cast<const AppLoaderApi*>(service_instance_get_data(instance));
}
// region Ad hoc "run this path directly" registry: owns the AppManifest (and its id/name/path
// strings) that app_manager's own ledger only keeps a non-owning pointer to (see
// app_manager_add()'s contract; install.cpp's InstallRegistry exists for the same reason).
// Keyed by path so re-running the same file reuses its manifest instead of re-adding it.
struct RunRecord {
std::string id;
std::string name;
std::string path;
AppManifest manifest {};
};
struct RunRegistry {
std::unordered_map<std::string, std::unique_ptr<RunRecord>> apps;
Mutex mutex {};
RunRegistry() { mutex_construct(&mutex); }
};
RunRegistry& run_registry() {
static RunRegistry registry;
return registry;
}
std::string last_path_segment(const std::string& path) {
auto index = path.find_last_of('/');
return index == std::string::npos ? path : path.substr(index + 1);
}
// A short, stable-per-path id that fits AppManifest::id's APP_ID_LENGTH limit regardless of how
// long @a path is. FNV-1a's collision odds are irrelevant at the scale of "distinct executables
// one user runs from the Files app".
std::string make_run_id(const std::string& path) {
uint32_t hash = 2166136261u;
for (unsigned char c: path) {
hash ^= c;
hash *= 16777619u;
}
char id[16];
std::snprintf(id, sizeof(id), "run.%08x", static_cast<unsigned int>(hash));
return id;
}
} // namespace
extern "C" {
error_t app_exec_path_add(const char* path) {
auto normalized = normalize(path);
auto& reg = registry();
mutex_lock(&reg.mutex);
for (size_t i = 0; i < reg.count; i++) {
if (reg.paths[i] == normalized) {
mutex_unlock(&reg.mutex);
return ERROR_NONE;
}
}
if (reg.count >= APP_EXEC_MAX_PATHS) {
mutex_unlock(&reg.mutex);
LOG_E(TAG, "Registry full, can't add %s", path);
return ERROR_OUT_OF_MEMORY;
}
reg.paths[reg.count++] = normalized;
mutex_unlock(&reg.mutex);
return ERROR_NONE;
}
error_t app_exec_path_remove(const char* path) {
auto normalized = normalize(path);
auto& reg = registry();
mutex_lock(&reg.mutex);
for (size_t i = 0; i < reg.count; i++) {
if (reg.paths[i] == normalized) {
reg.paths[i] = reg.paths[reg.count - 1];
reg.count--;
mutex_unlock(&reg.mutex);
return ERROR_NONE;
}
}
mutex_unlock(&reg.mutex);
return ERROR_NOT_FOUND;
}
bool app_exec_path_allowed(const char* file_path) {
std::string path = make_absolute(file_path);
if (has_dotdot_segment(path)) {
return false;
}
auto& reg = registry();
mutex_lock(&reg.mutex);
bool allowed = false;
for (size_t i = 0; i < reg.count; i++) {
if (is_under_directory(path, reg.paths[i])) {
allowed = true;
break;
}
}
mutex_unlock(&reg.mutex);
return allowed;
}
bool app_exec_is_executable_path(const char* path) {
const AppLoaderApi* loader = find_path_loader_api();
if (loader == nullptr || loader->is_executable == nullptr) {
return false;
}
AppLocation location { APP_LOCATION_PATH, const_cast<char*>(path) };
return loader->is_executable(location);
}
error_t app_exec_run_path(const char* path, int argc, const char* const argv[], AppInstanceId* out_app_instance_id) {
if (!app_exec_is_executable_path(path)) {
return ERROR_NOT_ALLOWED;
}
std::string path_str(path);
auto& reg = run_registry();
mutex_lock(&reg.mutex);
auto iterator = reg.apps.find(path_str);
std::string id;
if (iterator != reg.apps.end()) {
id = iterator->second->id;
mutex_unlock(&reg.mutex);
} else {
auto record = std::make_unique<RunRecord>();
record->id = make_run_id(path_str);
record->name = last_path_segment(path_str);
record->path = path_str;
record->manifest = AppManifest {
.id = record->id.c_str(),
.name = record->name.c_str(),
.category = APP_CATEGORY_USER,
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
.flags = APP_MANIFEST_FLAG_HIDDEN,
.stack = { .depth = 0, .desired_memory_capability = 0 },
};
error_t add_result = app_manager_add(&record->manifest);
if (add_result != ERROR_NONE) {
mutex_unlock(&reg.mutex);
LOG_E(TAG, "Failed to register %s: %s", path, error_to_string(add_result));
return add_result;
}
id = record->id;
reg.apps[path_str] = std::move(record);
mutex_unlock(&reg.mutex);
}
return app_manager_start_with_parameters(id.c_str(), argc, argv, out_app_instance_id);
}
void app_exec_register_default_paths() {
char data_root[192];
if (paths_get_data_path(data_root, sizeof(data_root)) == ERROR_NONE) {
app_exec_path_add((std::string(data_root) + "/bin").c_str());
app_exec_path_add((std::string(data_root) + "/app").c_str());
} else {
LOG_W(TAG, "Failed to resolve data path, not registering its executable directories");
}
}
} // extern "C"

View File

@ -24,10 +24,15 @@ int32_t api_run(AppRuntime runtime, uint32_t /*app_instance_id*/, int argc, char
void api_unload(AppRuntime /*unused*/) {
}
bool api_is_executable(AppLocation location) {
return location.type == APP_LOCATION_MEMORY && location.location != nullptr;
}
AppLoaderApi memory_loader_api = {
.load = api_load,
.run = api_run,
.unload = api_unload,
.is_executable = api_is_executable,
};
void* create_service(const ServiceManifest*) {

View File

@ -1,5 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/event.h>
#include <app/exec.h>
#include <app/install.h>
#include <app/io.h>
#include <app/manager.h>
@ -9,6 +10,8 @@
#include <app/scheduler.h>
#include <app/stream.h>
#include <app/private/exec_internal.h>
#include <service/manager.h>
#include <tactility/concurrent/task_event_group.h>
@ -73,7 +76,12 @@ static const ModuleSymbol SYMBOLS[] = {
};
static error_t start() {
return service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true);
error_t error = service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true);
if (error != ERROR_NONE) {
return error;
}
app_exec_register_default_paths();
return ERROR_NONE;
}
static error_t stop() {

View File

@ -0,0 +1,136 @@
// SPDX-License-Identifier: Apache-2.0
#include "doctest.h"
#include <app/exec.h>
#include <app/loader.h>
#include <app/location.h>
#include <service/instance.h>
#include <service/manager.h>
#include <climits>
#include <cstdio>
#include <string>
#include <unistd.h>
extern ServiceManifest app_internal_loader_service_manifest;
namespace {
const AppLoaderApi* memory_loader_api() {
if (service_manager_find_instance(APP_LOADER_MEMORY_SERVICE_ID) == nullptr) {
service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true);
}
auto* instance = service_manager_find_instance(APP_LOADER_MEMORY_SERVICE_ID);
REQUIRE(instance != nullptr);
return static_cast<const AppLoaderApi*>(service_instance_get_data(instance));
}
int32_t fake_entry(int, char*[]) {
return 0;
}
} // namespace
TEST_CASE("app_exec_path_add/app_exec_path_allowed accept nested children of a registered directory") {
REQUIRE_EQ(app_exec_path_add("/exec-test/bin"), ERROR_NONE);
CHECK(app_exec_path_allowed("/exec-test/bin/foo"));
CHECK(app_exec_path_allowed("/exec-test/bin/sub/foo"));
CHECK_FALSE(app_exec_path_allowed("/exec-test/binaries/foo"));
CHECK_FALSE(app_exec_path_allowed("/exec-test/bin"));
app_exec_path_remove("/exec-test/bin");
}
TEST_CASE("app_exec_path_allowed rejects a '..' segment even under a registered directory") {
REQUIRE_EQ(app_exec_path_add("/exec-test/bin2"), ERROR_NONE);
CHECK_FALSE(app_exec_path_allowed("/exec-test/bin2/../etc/passwd"));
app_exec_path_remove("/exec-test/bin2");
}
TEST_CASE("app_exec_path_add is a no-op for a path already registered") {
REQUIRE_EQ(app_exec_path_add("/exec-test/dup"), ERROR_NONE);
REQUIRE_EQ(app_exec_path_add("/exec-test/dup"), ERROR_NONE);
CHECK(app_exec_path_allowed("/exec-test/dup/foo"));
app_exec_path_remove("/exec-test/dup");
}
TEST_CASE("app_exec_path_add does not register same item twice") {
REQUIRE_EQ(app_exec_path_add("/exec-test/dup"), ERROR_NONE);
REQUIRE_EQ(app_exec_path_add("/exec-test/dup"), ERROR_NONE);
app_exec_path_remove("/exec-test/dup");
CHECK(!app_exec_path_allowed("/exec-test/dup/foo"));
}
TEST_CASE("app_exec_path_remove reports ERROR_NOT_FOUND for a path that was never registered") {
CHECK_EQ(app_exec_path_remove("/exec-test/never-registered"), ERROR_NOT_FOUND);
}
TEST_CASE("app_exec_path_remove makes a previously allowed path disallowed again") {
REQUIRE_EQ(app_exec_path_add("/exec-test/removable"), ERROR_NONE);
REQUIRE(app_exec_path_allowed("/exec-test/removable/foo"));
REQUIRE_EQ(app_exec_path_remove("/exec-test/removable"), ERROR_NONE);
CHECK_FALSE(app_exec_path_allowed("/exec-test/removable/foo"));
}
TEST_CASE("app_exec_path_add resolves a relative path to absolute, so it matches an absolute incoming path") {
char cwd[PATH_MAX];
REQUIRE(getcwd(cwd, sizeof(cwd)) != nullptr);
REQUIRE_EQ(app_exec_path_add("exec-test-relative/bin"), ERROR_NONE);
std::string absolute_child = std::string(cwd) + "/exec-test-relative/bin/foo";
CHECK(app_exec_path_allowed(absolute_child.c_str()));
app_exec_path_remove("exec-test-relative/bin");
CHECK_FALSE(app_exec_path_allowed(absolute_child.c_str()));
}
TEST_CASE("app_exec_path_allowed resolves a relative incoming path to absolute, so it matches an absolute registered directory") {
char cwd[PATH_MAX];
REQUIRE(getcwd(cwd, sizeof(cwd)) != nullptr);
std::string absolute_dir = std::string(cwd) + "/exec-test-relative2/bin";
REQUIRE_EQ(app_exec_path_add(absolute_dir.c_str()), ERROR_NONE);
CHECK(app_exec_path_allowed("exec-test-relative2/bin/foo"));
app_exec_path_remove(absolute_dir.c_str());
}
TEST_CASE("app_exec_path_add returns ERROR_OUT_OF_MEMORY once the registry is full") {
char path[32];
int added = 0;
for (added = 0; added < APP_EXEC_MAX_PATHS + 1; added++) {
snprintf(path, sizeof(path), "/exec-test/overflow%d", added);
if (app_exec_path_add(path) != ERROR_NONE) {
break;
}
}
CHECK_LT(added, APP_EXEC_MAX_PATHS + 1);
for (int i = 0; i < added; i++) {
snprintf(path, sizeof(path), "/exec-test/overflow%d", i);
app_exec_path_remove(path);
}
}
TEST_CASE("memory loader's is_executable() reports a non-null AppLocation as executable, a null one as not") {
const AppLoaderApi* loader = memory_loader_api();
REQUIRE(loader->is_executable != nullptr);
AppLocation runnable { APP_LOCATION_MEMORY, reinterpret_cast<void*>(fake_entry) };
CHECK(loader->is_executable(runnable));
AppLocation empty { APP_LOCATION_MEMORY, nullptr };
CHECK_FALSE(loader->is_executable(empty));
AppLocation wrong_type { APP_LOCATION_PATH, reinterpret_cast<void*>(fake_entry) };
CHECK_FALSE(loader->is_executable(wrong_type));
}

View File

@ -1,4 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/elf_check.h>
#include <app/exec.h>
#include <app/loader.h>
#include <app/location.h>
@ -27,6 +29,36 @@ bool is_regular_file(const std::string& path) {
return ::stat(path.c_str(), &path_stat) == 0 && S_ISREG(path_stat.st_mode);
}
#ifndef __APPLE__
constexpr ElfRequirements EXECUTABLE_REQUIREMENTS = {
.elf_class = ELF_CLASS_64,
.data = ELF_DATA_2LSB,
.type = ELF_TYPE_DYN,
#if defined(__x86_64__)
.machine = ELF_MACHINE_X86_64,
#elif defined(__aarch64__)
.machine = ELF_MACHINE_AARCH64,
#else
#error "Unsupported POSIX architecture for ELF machine check"
#endif
};
#endif
// Validates an already-resolved binary path (see resolve_app_path()) before it's handed to
// dlopen(): the extension and directory checks are cheap string comparisons, so the file is only
// opened as a last resort.
bool is_executable_file(const std::string& resolved_path) {
if (!resolved_path.ends_with(".so") || !app_exec_path_allowed(resolved_path.c_str())) {
return false;
}
#ifdef __APPLE__
// The simulator's app binaries are Mach-O on macOS, not ELF, so there is no header to check.
return is_regular_file(resolved_path);
#else
return elf_check_file(resolved_path.c_str(), &EXECUTABLE_REQUIREMENTS);
#endif
}
// location.location can be either an app's install directory or the .so file directly; the
// former resolves to the per-architecture binary at {dir}/elf/posix-{TACTILITY_POSIX_ARCH}.so,
// mirroring app_esp32_loader_service.cpp's resolve_elf_path().
@ -56,6 +88,11 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) {
return error;
}
if (!is_executable_file(app_path)) {
LOG_E(TAG, "Not executable: %s", app_path.c_str());
return ERROR_NOT_ALLOWED;
}
LOG_I(TAG, "Loading %s", app_path.c_str());
// RTLD_NOW: a missing symbol fails here, not mid-run(). RTLD_LOCAL: this app's own exported
@ -100,10 +137,24 @@ void api_unload(AppRuntime runtime_ptr) {
delete runtime;
}
bool api_is_executable(AppLocation location) {
if (location.type != APP_LOCATION_PATH) {
return false;
}
std::string app_path;
if (resolve_app_path(static_cast<const char*>(location.location), app_path) != ERROR_NONE) {
return false;
}
return is_executable_file(app_path);
}
AppLoaderApi loader_api = {
.load = api_load,
.run = api_run,
.unload = api_unload,
.is_executable = api_is_executable,
};
void* create_service(const ServiceManifest*) {

View File

@ -11,6 +11,10 @@ add_library(app_posix_module_test_fixture SHARED EXCLUDE_FROM_ALL ${CMAKE_CURREN
target_include_directories(app_posix_module_test_fixture PRIVATE ${CMAKE_SOURCE_DIR}/Modules/app-module/include)
set_target_properties(app_posix_module_test_fixture PROPERTIES POSITION_INDEPENDENT_CODE ON)
# A file with a ".so" extension but no ELF header, for is_executable() rejection tests.
set(NON_ELF_FIXTURE_PATH "${CMAKE_CURRENT_BINARY_DIR}/not-elf.so")
file(WRITE "${NON_ELF_FIXTURE_PATH}" "not an elf file")
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
add_executable(AppPosixModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})
add_dependencies(AppPosixModuleTests app_posix_module_test_fixture)
@ -18,6 +22,7 @@ add_dependencies(AppPosixModuleTests app_posix_module_test_fixture)
target_include_directories(AppPosixModuleTests PRIVATE ${DOCTESTINC})
target_compile_definitions(AppPosixModuleTests PRIVATE
FIXTURE_APP_PATH="$<TARGET_FILE:app_posix_module_test_fixture>"
FIXTURE_NON_ELF_PATH="${NON_ELF_FIXTURE_PATH}"
)
add_test(NAME AppPosixModuleTests COMMAND AppPosixModuleTests)

View File

@ -2,6 +2,7 @@
#include "doctest.h"
#include <app/event.h>
#include <app/exec.h>
#include <app/loader.h>
#include <app/manager.h>
#include <app/scheduler.h>
@ -11,6 +12,7 @@
#include <tactility/delay.h>
#include <atomic>
#include <string>
extern ServiceManifest loader_service_manifest; // app-posix-module's own
extern ServiceManifest app_internal_loader_service_manifest; // app-module's real memory loader
@ -29,6 +31,13 @@ void ensure_memory_loader_registered() {
}
}
std::string directory_of(const std::string& path) {
auto slash = path.find_last_of('/');
return slash == std::string::npos ? "." : path.substr(0, slash);
}
const std::string FIXTURE_DIR = directory_of(FIXTURE_APP_PATH);
bool wait_for_state(AppInstanceId id, AppInstanceState target, uint32_t timeout_ms) {
uint32_t waited = 0;
while (waited < timeout_ms) {
@ -93,6 +102,7 @@ int32_t parent_app_main(int, char*[]) {
TEST_CASE("app-posix-module's loader-path service dlopen()s a .so and calls its main(), which resolves a real Tactility symbol against the host") {
ensure_path_loader_registered();
ensure_memory_loader_registered();
REQUIRE_EQ(app_exec_path_add(FIXTURE_DIR.c_str()), ERROR_NONE);
g_fixture_result.store(-1, std::memory_order_relaxed);
g_fixture_result_received.store(false, std::memory_order_relaxed);
@ -111,3 +121,45 @@ TEST_CASE("app-posix-module's loader-path service dlopen()s a .so and calls its
app_manager_remove("test.posix.parent");
}
TEST_CASE("app_exec_is_executable_path() accepts a real .so in a registered directory") {
ensure_path_loader_registered();
REQUIRE_EQ(app_exec_path_add(FIXTURE_DIR.c_str()), ERROR_NONE);
CHECK(app_exec_is_executable_path(FIXTURE_APP_PATH));
}
TEST_CASE("app_exec_is_executable_path() rejects the same .so when its directory isn't registered") {
ensure_path_loader_registered();
app_exec_path_remove(FIXTURE_DIR.c_str());
CHECK_FALSE(app_exec_is_executable_path(FIXTURE_APP_PATH));
// Restore for any later test case that relies on it being registered.
app_exec_path_add(FIXTURE_DIR.c_str());
}
TEST_CASE("app_exec_is_executable_path() rejects a .so-named file with no ELF header") {
ensure_path_loader_registered();
auto non_elf_dir = directory_of(FIXTURE_NON_ELF_PATH);
REQUIRE_EQ(app_exec_path_add(non_elf_dir.c_str()), ERROR_NONE);
CHECK_FALSE(app_exec_is_executable_path(FIXTURE_NON_ELF_PATH));
app_exec_path_remove(non_elf_dir.c_str());
}
TEST_CASE("app_exec_is_executable_path() rejects a nonexistent path in a registered directory") {
ensure_path_loader_registered();
REQUIRE_EQ(app_exec_path_add(FIXTURE_DIR.c_str()), ERROR_NONE);
CHECK_FALSE(app_exec_is_executable_path((FIXTURE_DIR + "/does-not-exist.so").c_str()));
}
TEST_CASE("app_exec_is_executable_path() rejects an install-directory-shaped path missing its per-arch .so") {
ensure_path_loader_registered();
REQUIRE_EQ(app_exec_path_add(FIXTURE_DIR.c_str()), ERROR_NONE);
// FIXTURE_DIR itself has no elf/posix-<arch>.so under it, so resolution fails.
CHECK_FALSE(app_exec_is_executable_path(FIXTURE_DIR.c_str()));
}

View File

@ -30,8 +30,10 @@ class View final {
void showActionsForDirectory();
void showActionsForFile();
void showActionsForMountPoint();
void addCommonFileActions();
void viewFile(const std::string&path, const std::string&filename);
void runFile(const std::string& file_path);
void createDirEntryWidget(lv_obj_t* parent, dirent& dir_entry);
void onNavigate();
@ -54,6 +56,7 @@ public:
void onCutPressed();
void onPastePressed();
void onEjectPressed();
void onRunPressed();
void onDirEntryListScrollBegin();
void onResult(uint32_t launchId, int32_t result);
void deinit();

View File

@ -1,5 +1,6 @@
#include <app/install.h>
#include <app/event.h>
#include <app/exec.h>
#include <app/install.h>
#include <lvgl/lvgl.h>
#include <lvgl/widgets/toolbar.h>
@ -102,6 +103,11 @@ static void onPastePressedCallback(lv_event_t* event) {
view->onPastePressed();
}
static void onRunPressedCallback(lv_event_t* event) {
auto* view = static_cast<View*>(lv_event_get_user_data(event));
view->onRunPressed();
}
// endregion
// region File helpers
@ -192,6 +198,8 @@ void View::viewFile(const std::string& path, const std::string& filename) {
// Remove forward slash, because we need a relative path
notes::start(file_path.substr(1));
}
} else if (app_exec_is_executable_path(file_path.c_str())) {
runFile(file_path);
} else {
LOG_W(TAG, "Opening files of this type is not supported");
}
@ -199,6 +207,15 @@ void View::viewFile(const std::string& path, const std::string& filename) {
onNavigate();
}
void View::runFile(const std::string& file_path) {
LOG_I(TAG, "Running %s", file_path.c_str());
AppInstanceId instance_id = 0;
if (app_exec_run_path(file_path.c_str(), 0, nullptr, &instance_id) != ERROR_NONE) {
LOG_W(TAG, "Failed to run %s", file_path.c_str());
alertdialog::start(appInstanceId, "Run failed", "Could not run \"" + file::getLastPathSegment(file_path) + "\".");
}
}
bool View::resolveDirentFromListIndex(int32_t list_index, dirent& out_entry) {
const bool is_root = (state->getCurrentPath() == "/");
const bool has_back = (!is_root && current_start_index > 0);
@ -287,6 +304,8 @@ void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) {
symbol = LV_SYMBOL_IMAGE;
} else if (dir_entry.d_type == file::TT_DT_LNK) {
symbol = LV_SYMBOL_LOOP;
} else if (app_exec_is_executable_path(file::getChildPath(state->getCurrentPath(), dir_entry.d_name).c_str())) {
symbol = LV_SYMBOL_PLAY;
} else {
symbol = LV_SYMBOL_FILE;
}
@ -370,9 +389,7 @@ void View::onNewFolderPressed() {
inputdialog::start(appInstanceId, "New Folder", "Enter folder name:", "");
}
void View::showActions() {
lv_obj_clean(action_list);
void View::addCommonFileActions() {
auto* copy_button = lv_list_add_button(action_list, LV_SYMBOL_COPY, "Copy");
lv_obj_add_event_cb(copy_button, onCopyPressedCallback, LV_EVENT_SHORT_CLICKED, this);
auto* cut_button = lv_list_add_button(action_list, LV_SYMBOL_CUT, "Cut");
@ -381,12 +398,27 @@ void View::showActions() {
lv_obj_add_event_cb(rename_button, onRenamePressedCallback, LV_EVENT_SHORT_CLICKED, this);
auto* delete_button = lv_list_add_button(action_list, LV_SYMBOL_TRASH, "Delete");
lv_obj_add_event_cb(delete_button, onDeletePressedCallback, LV_EVENT_SHORT_CLICKED, this);
}
void View::showActions() {
lv_obj_clean(action_list);
addCommonFileActions();
lv_obj_remove_flag(action_list, LV_OBJ_FLAG_HIDDEN);
}
void View::showActionsForDirectory() { showActions(); }
void View::showActionsForFile() { showActions(); }
void View::showActionsForFile() {
lv_obj_clean(action_list);
if (app_exec_is_executable_path(state->getSelectedChildPath().c_str())) {
auto* run_button = lv_list_add_button(action_list, LV_SYMBOL_PLAY, "Run");
lv_obj_add_event_cb(run_button, onRunPressedCallback, LV_EVENT_SHORT_CLICKED, this);
}
addCommonFileActions();
lv_obj_remove_flag(action_list, LV_OBJ_FLAG_HIDDEN);
}
void View::showActionsForMountPoint() {
lv_obj_clean(action_list);
@ -397,6 +429,12 @@ void View::showActionsForMountPoint() {
lv_obj_remove_flag(action_list, LV_OBJ_FLAG_HIDDEN);
}
void View::onRunPressed() {
std::string file_path = state->getSelectedChildPath();
onNavigate();
runFile(file_path);
}
void View::onEjectPressed() {
std::string mount_path = state->getSelectedChildPath();
LOG_I(TAG, "Ejecting %s", mount_path.c_str());