La capacidad de ejecutar binarios reubicables de forma nativa en sistemas operativos es un problema fundamental en la computación, especialmente relevante en entornos de desarrollo y despliegue modernos como Nix, Buck o Bazel. Históricamente, la resolución de rutas para intérpretes dinámicos (PT_INTERP) y shebangs ha sido estática, lo que dificulta la creación de paquetes de software que puedan moverse libremente entre diferentes ubicaciones del sistema de archivos sin necesidad de recompilación o parcheo. La variable de entorno $ORIGIN es un intento de resolver esto en el espacio de usuario, pero su soporte directo en el kernel ha sido limitado.

Este artículo describe una propuesta para integrar soporte de $ORIGIN, y más generalmente, la selección programática de intérpretes, directamente en el kernel de Linux. La solución se apoya en eBPF y el subsistema binfmt_misc, permitiendo una flexibilidad sin precedentes en cómo el kernel localiza y carga los intérpretes de los ejecutables. Esto no solo resuelve el problema de $ORIGIN, sino que abre la puerta a una nueva clase de binarios auto-contenidos y agnósticos a la ruta, mejorando la portabilidad y la gestión de dependencias en sistemas complejos.

Arquitectura del Sistema

La solución propuesta se articula en torno a dos componentes clave del kernel de Linux: eBPF y binfmt_misc. eBPF (extended Berkeley Packet Filter) permite ejecutar programas definidos por el usuario dentro del kernel en puntos de enganche predefinidos, con garantías de seguridad y rendimiento. Estos programas se escriben en un subconjunto de C, se compilan a bytecode y se verifican antes de su carga, y luego se JIT-compilan a código nativo para su ejecución.

El subsistema binfmt_misc es una interfaz del kernel que permite registrar manejadores para tipos de archivos ejecutables no nativos (por ejemplo, ejecutables de otras arquitecturas o scripts). Tradicionalmente, binfmt_misc redirige la ejecución a un intérprete registrado. La innovación aquí es la capacidad de usar un programa eBPF para implementar la lógica de 'match' y 'load' de binfmt_misc. Un programa eBPF se registra con el kernel, conteniendo funciones como nix_match (que identifica si un binario es relevante, por ejemplo, un archivo ELF o uno con un segmento PT_INTERP_NIX específico) y nix_load (que determina la ruta del intérprete dinámico, potencialmente derivándola de la ruta del binario usando bpf_path_d_path y bpf_binprm_set_interp).

Además, se introduce un nuevo modo de despacho 'L' en binfmt_misc. Este modo permite que el kernel ejecute el binario original como imagen principal, y solo sustituya el intérprete registrado por el cargador especificado en PT_INTERP del binario. Esto resuelve un problema de transparencia donde el intérprete registrado tradicionalmente se convertía en el proceso principal, alterando argv[0], /proc/<pid>/cmdline y /proc/self/exe, lo que causaba problemas para programas que se auto-localizan. Con el modo 'L', binfmt_misc actúa como un simple override de PT_INTERP, manteniendo la identidad del proceso original.

Flujo de Carga de Binario con binfmt_misc y eBPF

  1. 1 Ejecución de Binario El usuario intenta ejecutar un binario (ej. `./my_nix_app`)
  2. 2 Kernel: VFS El subsistema VFS del kernel intercepta la llamada `execve`
  3. 3 Kernel: binfmt_misc binfmt_misc es consultado para ver si hay un manejador registrado
  4. 4 eBPF: nix_match El programa eBPF `nix_match` se ejecuta para identificar el tipo de binario (...
  5. 5 eBPF: nix_load Si coincide, `nix_load` se ejecuta para determinar la ruta del intérprete din...
  6. 6 Kernel: Carga (Modo 'L') El kernel carga el binario original y sustituye el PT_INTERP con el intérpret...
  7. 7 Ejecución El binario se ejecuta con el intérprete dinámico correcto, manteniendo su ide...
CapaTecnologíaJustificación
orchestration Nix Sistema de gestión de paquetes y construcción que se beneficia directamente de binarios reubicables y agnósticos a la ruta. vs Docker, Snap, Flatpak
security eBPF Verifier Componente del kernel que garantiza la seguridad y terminación de los programas eBPF antes de su ejecución, previniendo vulnerabilidades. vs Módulos del kernel (menos seguro, más complejo)
compute Linux Kernel (VFS, binfmt_misc) Proporciona la infraestructura subyacente para la carga de ejecutables y la extensión de su comportamiento mediante eBPF. vs Otros kernels (BSD, Windows)

Trade-offs

Ganancias
  • Flexibilidad en la resolución de intérpretes
  • Portabilidad de binarios
  • Transparencia de la identidad del proceso
Costes
  • Complejidad del kernel (eBPF, binfmt_misc)
  • Curva de aprendizaje para desarrolladores
SEC("struct_ops.s/match")
bool BPF_PROG(nix_match, struct linux_binprm *bprm)
{
  return !bpf_strncmp(bprm->buf, 4, "\x7f" "ELF");
}

SEC("struct_ops.s/load")
int BPF_PROG(nix_load, struct linux_binprm *bprm)
{
  char path[256];
  long n;
  n = bpf_path_d_path(&bprm->file->f_path, path, sizeof(path));
  if (n < 0)
    return n;
  /* derive the loader location from the binary's path */
  return bpf_binprm_set_interp(bprm, path, sizeof(path));
}

SEC(".struct_ops.link")
struct binfmt_misc_ops nix = {
  .match = (void *)nix_match,
  .load = (void *)nix_load,
  .name = "nix",
};
Programa eBPF que define las funciones `match` y `load` para el subsistema binfmt_misc, permitiendo la detección de archivos ELF y la derivación dinámica de la ruta del intérprete.

Fundamentos Teóricos

El concepto de binarios reubicables y la resolución dinámica de dependencias se relaciona con problemas clásicos de vinculación y carga de programas, estudiados en la ciencia de la computación desde los primeros sistemas operativos. La idea de un 'cargador dinámico' (dynamic linker/loader) que resuelve símbolos y dependencias en tiempo de ejecución es fundamental y se remonta a trabajos como los de John R. Mashey sobre el sistema de archivos de Unix y la gestión de librerías compartidas en los años 70 y 80. La complejidad de la resolución de rutas y la gestión de entornos de ejecución aislados también se aborda en sistemas de empaquetado como Nix, que se inspira en principios de programación funcional y sistemas de construcción deterministas.

La utilización de eBPF para extender las capacidades del kernel se alinea con el concepto de 'microkernels' o 'extensible kernels', donde la funcionalidad se puede añadir o modificar de forma segura y eficiente sin recompilar el kernel completo. Aunque eBPF no convierte Linux en un microkernel, proporciona un mecanismo de extensión programable que recuerda los principios de sistemas como Mach o Exokernel, donde las aplicaciones tienen más control sobre los recursos del sistema. La verificación de programas eBPF para garantizar la seguridad y la terminación es un ejemplo de aplicación práctica de la verificación formal de programas, un área de investigación activa en la teoría de la computación.