From 3dd55ba34d651eaddca77f380f592415a734d4cf Mon Sep 17 00:00:00 2001 From: Axel Ibarrondo Date: Sat, 1 Aug 2026 23:49:55 -0300 Subject: [PATCH] setup: initial setup of the repo --- .github/workflows/ci.yml | 20 +++++++++++++ .gitignore | 1 + CMakeLists.txt | 59 +++++++++++++++++++++++++++++++++++++++ Makefile | 38 +++++++++++++++++++++++++ README.md | 41 ++++++++++++++++++++++++++- docs/adr/.gitkeep | 0 include/omicron/policy.h | 41 +++++++++++++++++++++++++++ include/omicron/pty.h | 31 ++++++++++++++++++++ include/omicron/sandbox.h | 23 +++++++++++++++ src/main.c | 13 +++++++++ src/policy.c | 49 ++++++++++++++++++++++++++++++++ src/pty.c | 45 +++++++++++++++++++++++++++++ src/sandbox_darwin.c | 30 ++++++++++++++++++++ src/sandbox_linux.c | 31 ++++++++++++++++++++ tests/sandbox_null.c | 26 +++++++++++++++++ tests/test_fdleak.c | 10 +++++++ tests/test_policy.c | 8 ++++++ tests/test_pty.c | 8 ++++++ tests/test_sandbox.c | 10 +++++++ 19 files changed, 483 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 CMakeLists.txt create mode 100644 Makefile create mode 100644 docs/adr/.gitkeep create mode 100644 include/omicron/policy.h create mode 100644 include/omicron/pty.h create mode 100644 include/omicron/sandbox.h create mode 100644 src/main.c create mode 100644 src/policy.c create mode 100644 src/pty.c create mode 100644 src/sandbox_darwin.c create mode 100644 src/sandbox_linux.c create mode 100644 tests/sandbox_null.c create mode 100644 tests/test_fdleak.c create mode 100644 tests/test_policy.c create mode 100644 tests/test_pty.c create mode 100644 tests/test_sandbox.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c50371f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,20 @@ +name: ci + +on: + push: + branches: [main] + pull_request: + +jobs: + build: + # Ninguno de los dos puede probar la plataforma del otro en local: + # esta matriz es lo único que avisa cuando se rompe la otra. + strategy: + fail-fast: false + matrix: + os: [macos-latest, ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - run: make build + - run: make test diff --git a/.gitignore b/.gitignore index 0ba1c63..439774e 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ CMakeFiles/ CMakeCache.txt cmake_install.cmake Makefile +!/Makefile install_manifest.txt compile_commands.json diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..90924a3 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,59 @@ +cmake_minimum_required(VERSION 3.20) +project(omicron LANGUAGES C) + +# Este archivo lista TODOS los archivos del proyecto, incluidos los vacíos. +# La idea es no volver a tocarlo: es el que más conflictos de git genera. + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS ON) # gnu11: hace falta para POSIX/BSD (termios, ioctl) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE) +endif() + +option(OMICRON_WERROR "Tratar warnings como errores" ON) +add_compile_options(-Wall -Wextra -Wshadow -Wvla) +if(OMICRON_WERROR) + add_compile_options(-Werror) +endif() + +include_directories(include) + +# --- Selección de plataforma ------------------------------------------------- +# Nunca un sandbox_generic.c que no haga nada: un binario que dice que aísla +# y no aísla es peor que no tener binario. Si no hay implementación, se aborta. +if(APPLE) + set(OM_SANDBOX_SRC src/sandbox_darwin.c) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(OM_SANDBOX_SRC src/sandbox_linux.c) +else() + message(FATAL_ERROR + "Omicron: no hay sandbox para ${CMAKE_SYSTEM_NAME}. " + "Se aborta antes que generar un binario sin aislamiento real.") +endif() + +# --- Binario ----------------------------------------------------------------- +add_executable(omicron + src/main.c + src/policy.c + src/pty.c + ${OM_SANDBOX_SRC} +) + +# --- Tests ------------------------------------------------------------------- +# tests/sandbox_null.c es un doble de test. Solo aparece en estos targets: +# el binario omicron no tiene forma de linkearlo. +enable_testing() + +add_executable(test_policy tests/test_policy.c src/policy.c) +add_executable(test_pty tests/test_pty.c src/pty.c src/policy.c tests/sandbox_null.c) +add_executable(test_fdleak tests/test_fdleak.c src/pty.c src/policy.c tests/sandbox_null.c) +add_executable(test_sandbox tests/test_sandbox.c src/policy.c ${OM_SANDBOX_SRC}) + +foreach(t test_policy test_pty test_fdleak test_sandbox) + add_test(NAME ${t} COMMAND ${t}) + # 77 = el test todavía es un stub; ctest lo reporta como Skipped, no como Passed. + set_tests_properties(${t} PROPERTIES SKIP_RETURN_CODE 77) +endforeach() diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a32b471 --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +# Wrapper del flujo de CMake. Igual en macOS y en Linux. +# Uso: make | make test | make run ARGS="-- /bin/sh" | make clean + +BUILD_DIR ?= build +BUILD_TYPE ?= Debug +CMAKE ?= cmake +CTEST ?= ctest +JOBS ?= $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) + +.PHONY: all configure build test run clean rebuild help + +all: build + +configure: $(BUILD_DIR)/CMakeCache.txt + +$(BUILD_DIR)/CMakeCache.txt: + $(CMAKE) -S . -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) + ln -sf $(BUILD_DIR)/compile_commands.json compile_commands.json + +build: configure + $(CMAKE) --build $(BUILD_DIR) --parallel $(JOBS) + +test: build + $(CTEST) --test-dir $(BUILD_DIR) --output-on-failure + +run: build + ./$(BUILD_DIR)/omicron $(ARGS) + +clean: + rm -rf $(BUILD_DIR) + +rebuild: clean build + +help: + @echo "make compila" + @echo "make test compila y corre los tests" + @echo "make run ARGS=\"--allow-rw /tmp -- /bin/sh\"" + @echo "make clean borra $(BUILD_DIR)/" diff --git a/README.md b/README.md index e4825c5..aa9bcd3 100644 --- a/README.md +++ b/README.md @@ -1 +1,40 @@ -# Terminal \ No newline at end of file +# Omicron + +Terminal Unix con sandbox: el proceso hijo se lanza con capacidades reducidas +por el kernel, no filtrando el texto del comando. + +## Build + +``` +make # compila +make test # compila y corre los tests +make run ARGS="--allow-rw /tmp -- /bin/sh" +``` + +Requiere CMake >= 3.20 y un compilador C11. macOS y Linux. + +## Estructura + +``` +include/omicron/ contrato: policy.h, sandbox.h, pty.h +src/ implementación (un sandbox por plataforma) +tests/ ctest +docs/adr/ decisiones arquitectónicas +``` + +## Reparto + +| Archivo | Dueño | +|---|---| +| `src/main.c`, `src/policy.c`, `src/sandbox_darwin.c` | axel | +| `src/pty.c`, `src/sandbox_linux.c` | LevtCode | +| `include/`, `CMakeLists.txt` | compartidos: PR aprobado por ambos | +| `tests/test_*.c` | el dueño del módulo | + +## Meta v1 + +``` +omicron --allow-ro /usr --allow-rw /tmp -- /bin/sh +``` + +Adentro: `ls /tmp` funciona, `cat ~/.ssh/id_rsa` da `EPERM`. diff --git a/docs/adr/.gitkeep b/docs/adr/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/include/omicron/policy.h b/include/omicron/policy.h new file mode 100644 index 0000000..e1276f6 --- /dev/null +++ b/include/omicron/policy.h @@ -0,0 +1,41 @@ +/* Política del sandbox: describe qué se le permite al proceso aislado. + * Son datos puros. No llama al kernel ni protege nada por sí sola: + * la traducen sandbox_darwin.c (Seatbelt) y sandbox_linux.c (Landlock). */ +#ifndef OMICRON_POLICY_H +#define OMICRON_POLICY_H + +#include +#include +#include + +#ifndef OM_POLICY_FWD +#define OM_POLICY_FWD +typedef struct om_policy om_policy; +#endif + +struct om_policy { + char **ro_paths; /* rutas de solo lectura */ + size_t ro_count; + char **rw_paths; /* rutas de lectura y escritura */ + size_t rw_count; + bool allow_net; + rlim_t max_procs; + rlim_t max_mem; /* bytes */ +}; + +/* Política mínima razonable. NULL si falta memoria. */ +om_policy *om_policy_default(void); + +/* Agregan una ruta. 0 ok, -1 error (errno). La ruta se copia. */ +int om_policy_add_ro(om_policy *p, const char *path); +int om_policy_add_rw(om_policy *p, const char *path); + +/* Parsea flags hasta "--". Deja en *cmd_index el índice del comando en argv. */ +int om_policy_parse_args(om_policy *p, int argc, char **argv, int *cmd_index); + +/* Canonicaliza con realpath y rechaza lo inseguro. 0 ok, -1 rechazada. */ +int om_policy_validate(om_policy *p); + +void om_policy_free(om_policy *p); + +#endif /* OMICRON_POLICY_H */ diff --git a/include/omicron/pty.h b/include/omicron/pty.h new file mode 100644 index 0000000..ab73032 --- /dev/null +++ b/include/omicron/pty.h @@ -0,0 +1,31 @@ +/* PTY: crea el par master/slave, lanza el hijo ya aislado y bombea E/S. */ +#ifndef OMICRON_PTY_H +#define OMICRON_PTY_H + +#include + +#ifndef OM_POLICY_FWD +#define OM_POLICY_FWD +typedef struct om_policy om_policy; +#endif + +/* Códigos de salida reservados del hijo: distinguen "no se ejecutó" de "falló". */ +#define OM_EXIT_SANDBOX 127 /* el sandbox no se pudo aplicar; nunca hubo exec */ +#define OM_EXIT_EXEC 126 /* el sandbox se aplicó pero el exec falló */ + +typedef struct { + int master_fd; + pid_t pid; +} om_pty; + +/* fork + setsid + TIOCSCTTY + cerrar fds + om_sandbox_apply + execvp. */ +int om_pty_spawn(char *const argv[], const om_policy *p, om_pty *t); + +/* Bombea entre la terminal real y master_fd hasta que el hijo termine. */ +int om_pty_pump(om_pty *t); + +int om_pty_resize(om_pty *t, unsigned rows, unsigned cols); +int om_pty_wait(om_pty *t, int *status); +void om_pty_kill(om_pty *t); + +#endif /* OMICRON_PTY_H */ diff --git a/include/omicron/sandbox.h b/include/omicron/sandbox.h new file mode 100644 index 0000000..2a325d5 --- /dev/null +++ b/include/omicron/sandbox.h @@ -0,0 +1,23 @@ +/* Frontera de seguridad. Una firma, dos implementaciones (darwin / linux). + * Nada fuera de estos tres símbolos sabe qué mecanismo de kernel se usa. */ +#ifndef OMICRON_SANDBOX_H +#define OMICRON_SANDBOX_H + +#ifndef OM_POLICY_FWD +#define OM_POLICY_FWD +typedef struct om_policy om_policy; +#endif + +/* Opaco: cada plataforma define su propio contenido en su .c */ +typedef struct om_sandbox om_sandbox; + +/* Corre en el PADRE, antes del fork. Puede reservar memoria. NULL si falla. */ +om_sandbox *om_sandbox_prepare(const om_policy *p); + +/* Corre en el HIJO, entre fork y exec. Irreversible. Sin malloc. + * 0 ok, -1 error: el hijo debe hacer _exit(OM_EXIT_SANDBOX). */ +int om_sandbox_apply(const om_sandbox *s); + +void om_sandbox_free(om_sandbox *s); + +#endif /* OMICRON_SANDBOX_H */ diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..1830e3c --- /dev/null +++ b/src/main.c @@ -0,0 +1,13 @@ +/* Dueño: axel. Solo orquesta: policy -> spawn -> pump -> wait. */ +#include "omicron/policy.h" +#include "omicron/pty.h" + +#include + +int main(int argc, char **argv) +{ + (void)argc; + (void)argv; + fputs("omicron: sin implementar\n", stderr); + return 1; +} diff --git a/src/policy.c b/src/policy.c new file mode 100644 index 0000000..81990a2 --- /dev/null +++ b/src/policy.c @@ -0,0 +1,49 @@ +/* Dueño: axel. Portable: no toca APIs de sandbox. */ +#include "omicron/policy.h" + +#include +#include + +om_policy *om_policy_default(void) +{ + errno = ENOSYS; + return NULL; +} + +int om_policy_add_ro(om_policy *p, const char *path) +{ + (void)p; + (void)path; + errno = ENOSYS; + return -1; +} + +int om_policy_add_rw(om_policy *p, const char *path) +{ + (void)p; + (void)path; + errno = ENOSYS; + return -1; +} + +int om_policy_parse_args(om_policy *p, int argc, char **argv, int *cmd_index) +{ + (void)p; + (void)argc; + (void)argv; + (void)cmd_index; + errno = ENOSYS; + return -1; +} + +int om_policy_validate(om_policy *p) +{ + (void)p; + errno = ENOSYS; + return -1; +} + +void om_policy_free(om_policy *p) +{ + (void)p; +} diff --git a/src/pty.c b/src/pty.c new file mode 100644 index 0000000..0c7b50d --- /dev/null +++ b/src/pty.c @@ -0,0 +1,45 @@ +/* Dueño: LevtCode. Portable: usar posix_openpt, no openpty (evita #ifdef). */ +#include "omicron/pty.h" + +#include "omicron/sandbox.h" + +#include + +int om_pty_spawn(char *const argv[], const om_policy *p, om_pty *t) +{ + (void)argv; + (void)p; + (void)t; + /* Orden obligatorio en el hijo: cerrar fds ANTES de om_sandbox_apply. */ + errno = ENOSYS; + return -1; +} + +int om_pty_pump(om_pty *t) +{ + (void)t; + errno = ENOSYS; + return -1; +} + +int om_pty_resize(om_pty *t, unsigned rows, unsigned cols) +{ + (void)t; + (void)rows; + (void)cols; + errno = ENOSYS; + return -1; +} + +int om_pty_wait(om_pty *t, int *status) +{ + (void)t; + (void)status; + errno = ENOSYS; + return -1; +} + +void om_pty_kill(om_pty *t) +{ + (void)t; +} diff --git a/src/sandbox_darwin.c b/src/sandbox_darwin.c new file mode 100644 index 0000000..d0f3688 --- /dev/null +++ b/src/sandbox_darwin.c @@ -0,0 +1,30 @@ +/* Dueño: axel. Seatbelt: generar perfil SBPL en prepare, sandbox_init en apply. */ +#include "omicron/sandbox.h" + +#include "omicron/policy.h" + +#include +#include + +struct om_sandbox { + char *profile; /* texto SBPL, construido antes del fork */ +}; + +om_sandbox *om_sandbox_prepare(const om_policy *p) +{ + (void)p; + errno = ENOSYS; + return NULL; +} + +int om_sandbox_apply(const om_sandbox *s) +{ + (void)s; + errno = ENOSYS; + return -1; +} + +void om_sandbox_free(om_sandbox *s) +{ + free(s); +} diff --git a/src/sandbox_linux.c b/src/sandbox_linux.c new file mode 100644 index 0000000..e672c92 --- /dev/null +++ b/src/sandbox_linux.c @@ -0,0 +1,31 @@ +/* Dueño: LevtCode. Landlock + seccomp + rlimits. + * PR_SET_NO_NEW_PRIVS va antes de restringir, o el kernel lo rechaza. */ +#include "omicron/sandbox.h" + +#include "omicron/policy.h" + +#include +#include + +struct om_sandbox { + int ruleset_fd; /* fd del ruleset de Landlock, abierto antes del fork */ +}; + +om_sandbox *om_sandbox_prepare(const om_policy *p) +{ + (void)p; + errno = ENOSYS; + return NULL; +} + +int om_sandbox_apply(const om_sandbox *s) +{ + (void)s; + errno = ENOSYS; + return -1; +} + +void om_sandbox_free(om_sandbox *s) +{ + free(s); +} diff --git a/tests/sandbox_null.c b/tests/sandbox_null.c new file mode 100644 index 0000000..4dbe156 --- /dev/null +++ b/tests/sandbox_null.c @@ -0,0 +1,26 @@ +/* Doble de test: sandbox que no hace nada. Permite testear pty.c aislado. + * SOLO se linkea en binarios de test; el binario omicron no puede usarlo. */ +#include "omicron/sandbox.h" + +#include + +struct om_sandbox { + int unused; +}; + +om_sandbox *om_sandbox_prepare(const om_policy *p) +{ + (void)p; + return calloc(1, sizeof(om_sandbox)); +} + +int om_sandbox_apply(const om_sandbox *s) +{ + (void)s; + return 0; +} + +void om_sandbox_free(om_sandbox *s) +{ + free(s); +} diff --git a/tests/test_fdleak.c b/tests/test_fdleak.c new file mode 100644 index 0000000..73c6ca0 --- /dev/null +++ b/tests/test_fdleak.c @@ -0,0 +1,10 @@ +/* Protege la costura pty <-> sandbox: un fd heredado atraviesa el sandbox + * entero, porque Landlock y Seatbelt restringen abrir rutas, no fds ya abiertos. + * El hijo se autoinspecciona con fcntl(fd, F_GETFD) de 3 a 255. */ +#include "omicron/pty.h" + +int main(void) +{ + /* TODO: spawn y exigir status 0 = solo 0, 1 y 2 abiertos en el hijo */ + return 77; /* borrar esta línea al implementar (77 = SKIP en ctest) */ +} diff --git a/tests/test_policy.c b/tests/test_policy.c new file mode 100644 index 0000000..5154eb0 --- /dev/null +++ b/tests/test_policy.c @@ -0,0 +1,8 @@ +/* Dueño: axel. Sin fork ni privilegios: corre en las dos plataformas. */ +#include "omicron/policy.h" + +int main(void) +{ + /* TODO: realpath desenmascara "/tmp/../home" -> om_policy_validate == -1 */ + return 77; /* borrar esta línea al implementar (77 = SKIP en ctest) */ +} diff --git a/tests/test_pty.c b/tests/test_pty.c new file mode 100644 index 0000000..d82c7ca --- /dev/null +++ b/tests/test_pty.c @@ -0,0 +1,8 @@ +/* Dueño: LevtCode. Linkea sandbox_null.c: no depende del sandbox real. */ +#include "omicron/pty.h" + +int main(void) +{ + /* TODO: spawn de /bin/echo y verificar lo que sale por master_fd */ + return 77; /* borrar esta línea al implementar (77 = SKIP en ctest) */ +} diff --git a/tests/test_sandbox.c b/tests/test_sandbox.c new file mode 100644 index 0000000..74a6424 --- /dev/null +++ b/tests/test_sandbox.c @@ -0,0 +1,10 @@ +/* Linkea el sandbox REAL de la plataforma. Mismas aserciones en macOS y Linux; + * si algún día divergen, el contrato de sandbox.h está mal. */ +#include "omicron/policy.h" +#include "omicron/sandbox.h" + +int main(void) +{ + /* TODO: abrir una ruta fuera de la policy debe fallar con EPERM */ + return 77; /* borrar esta línea al implementar (77 = SKIP en ctest) */ +}