GCC fragmenta rutas en binarios y rompe las auditorías

Por qué un binario compilado con GCC puede esconder la ruta de build

Un binario compilado con GCC 12 o posterior puede llevar dentro la ruta /home/linuxbrew/.linuxbrew y aun así devolver cero coincidencias al buscarla con grep -acF o con strings | grep -cF. No falla la herramienta de búsqueda ni el binario: el compilador partió la cadena en pedazos y la guardó de una forma que ningún escaneo de texto plano puede reconstruir. Si tu pipeline de integración continua rechaza builds con rutas absolutas de la máquina de compilación, ese chequeo puede estar dando luz verde a artefactos que no van a funcionar en la máquina del usuario.

El caso está descrito en detalle en un análisis publicado el 12 de noviembre de 2026 en stoppels.ch, escrito por un desarrollador que ya abrió un hilo en la lista de correo gcc-patches con un parche inicial para poder desactivar esta optimización.

El ejemplo que usa es mínimo: un programa en C++ que construye un std::string a partir de una constante con la ruta /home/linuxbrew/.linuxbrew. Compilado con g++ -O2, el programa imprime la ruta correctamente. Pero al buscar esos bytes en el binario:

¿Y esto cómo se aplica en tu negocio?

Dueños de negocio de Latinoamérica y España usan un agente de IA para ganar más y recuperar su tiempo. El Kit «tu mano derecha con IA» te muestra cómo en un video de 11 minutos, con una ficha para armar el tuyo. CAR está cerrada por ahora; al anotarte, te avisamos cuando abra.

👥 Quiero el Kit →
  • grep -acF /home/linuxbrew/.linuxbrew main devuelve 0
  • strings ./main | grep -cF /home/linuxbrew/.linuxbrew devuelve 0

Y si buscas solo el prefijo /home, aparece algo todavía más incómodo: /home/linuxbrew/xbrew/.linuxbrew. El subdirectorio xbrew no existe en ningún lado del proyecto. Es el resultado de que el compilador pegó fragmentos de la cadena en un orden que produce texto plausible pero falso.

¿Quiénes auditan binarios en busca de rutas absolutas?

La lista de sistemas que dependen de este tipo de escaneo cubre buena parte del ecosistema de empaquetado:

  • RPM (Fedora, RHEL, openSUSE): check-buildroot hace grep del build root en cada archivo instalado.
  • Arch Linux: makepkg avisa de referencias a $srcdir y $pkgdir.
  • Yocto / OpenEmbedded: el chequeo de QA buildpaths.
  • FreeBSD ports: el stage QA busca el directorio de staging.
  • pkgsrc: CHECK_WRKREF, desactivado salvo que el mantenedor del paquete lo active.
  • Homebrew: brew bottle escanea el prefijo para decidir si un bottle es relocalizable.
  • conda-build: los archivos que contienen el prefijo de build se listan en info/has_prefix para reescribirlos al instalar.
  • rattler-build: el mismo esquema, con memmem.
  • BinaryBuilder de Julia: check_absolute_paths avisa del prefijo de build en cualquier archivo.

Todos esos chequeos comparten la misma suposición: que la ruta está literalmente en el binario. Con GCC sobre x86-64, esa suposición no se sostiene.

¿Cómo guarda GCC una cadena sin guardarla?

La explicación está en cómo GCC optimiza la copia de constantes de string. Para std::string{PREFIX}, el compilador divide la cadena en dos piezas solapadas de 16 bytes que caben exactamente en registros xmm, y luego copia el contenido al buffer del std::string con instrucciones de movimiento vectorizado. En el ensamblador generado aparecen cargas del tipo movdqa xmm0, XMMWORD PTR .LC0[rip] seguidas de movups hacia el buffer, y las constantes quedan guardadas como pares de valores .quad que, además, terminan una al lado de otra en el binario.

El detalle clave es cuándo ocurre. La división en piezas aparece cuando la constante de string se copia a un buffer: std::string, strcpy, memcpy. En cambio, las cadenas que solo se pasan como puntero, como en fopen(PREFIX "/x", "r"), se mantienen intactas y sí aparecen en un strings.

La deduplicación que borra el prefijo de Conda

El problema se agrava con los prefijos largos. conda-build y Spack instalan paquetes en un prefijo rellenado durante el build, con la idea de poder relocalizarlos después a un prefijo más corto elegido por el usuario: los binarios se parchean in-place. Conda usa la cadena repetida _placehold dentro de un nombre de directorio para que el prefijo sea lo bastante largo.

La prueba del artículo usa una ruta de 256 bytes (múltiplo cómodo de todos los anchos de registro de x86-64). Resultado: en el binario no aparece la ruta completa, sino una cadena de 176 bytes, es decir 11 veces 16. Como _placehold se repite cada 10 bytes, las piezas de 16 bytes se repiten cada lcm(10, 16) = 80 bytes, y a GCC le alcanza con almacenar 5 constantes distintas para todo el relleno. El resto es copia repetida.

Para quien empaqueta, la consecuencia es directa: el parcheo in-place de rutas, que en Conda y Spack funciona como una invocación de sed glorificada, solo puede funcionar si la ruta está en una sola pieza. Con esta optimización, muchas veces no lo está.

¿Clang hace lo mismo y qué gana GCC?

Clang no divide los strings en constantes y aun así genera esencialmente las mismas instrucciones. En el ejemplo con una ruta más larga y target AVX2 (-march=x86-64-v3), Clang deja el string entero en la sección .rodata.str1.1 y copia sus 53 bytes con dos cargas y stores solapados de 32 bytes en ymm, en offsets 21 y 0. GCC hace lo mismo, pero leyendo dos piezas desde .rodata.cst32.

En rendimiento, la diferencia es probablemente inmedible: ambos hacen las mismas cargas y stores. GCC podría tener una leve ventaja con piezas repetidas, porque carga una pieza una vez y la almacena varias veces (11 cargas para 16 stores en el ejemplo del prefijo de 256 bytes), y usa lecturas alineadas, algo que solo importa en CPUs muy antiguas.

En tamaño, en cambio, las piezas de GCC pueden ocupar más que el string: los bytes solapados se guardan dos veces y las piezas se alinean a 16, 32 o 64 bytes. La ruta de 53 bytes ocupa 64 bytes como dos piezas, contra 54 como string. Solo compensa si hay deduplicación real, por ejemplo cuando muchas cadenas comparten un prefijo de al menos 16 bytes (SSE2), 32 (AVX2) o 64 (AVX-512).

¿Se puede desactivar esta optimización?

Prácticamente no. Aparte de compilar con -Os, no hay una opción limpia. Desde GCC 10 (y posiblemente versiones anteriores) la optimización se activa para cadenas de hasta 256 bytes aunque no especifiques -march. Desde GCC 12 depende del ancho vectorial del target, así que las cadenas largas también se parten: hasta 512 bytes con AVX2 y 1024 bytes con AVX-512.

El único control disponible es fijar el ancho vectorial máximo en 128 bits con -mstore-max=128, lo que fuerza operaciones xmm. Pero eso solo restaura los valores por defecto antiguos: las cadenas de hasta 256 bytes siguen guardándose en piezas.

¿Las auditorías post-build son una mala práctica?

Es una pregunta legítima. Debian va mucho más allá de un escaneo de rutas: su auditoría exige que un paquete se pueda reconstruir bit a bit en circunstancias distintas (otra máquina, otro usuario, otra fecha y hora, otro hostname, otro locale). Es una forma mucho más fiable de mantener detalles específicos de la máquina fuera de los binarios.

Aun así, el autor defiende mantener las constantes de string literales en el binario, incluso en Debian, porque mejora la interpretabilidad: correr strings ./binario es una forma simple de asomarse a una caja negra, y eso compensa el ahorro marginal de la división y la deduplicación. La comparación que usa es la de Fedora y Ubuntu volviendo a activar los frame pointers por defecto, pese al pequeño overhead, para que los profilers y las herramientas de tracing funcionen de forma más fiable.

Hay un matiz incómodo incluso en Debian: desde principios de 2024, ambas builds usan el mismo directorio de compilación, porque queda registrado en el archivo .buildinfo y se reutiliza al reconstruir. Es decir, una ruta de build embebida en un binario también puede pasar desapercibida ahí.

¿Qué significa esto para tu startup?

Si distribuyes software compilado, ya sea un CLI, un paquete de conda, un bottle de Homebrew o una extensión nativa dentro de un wheel, este detalle cambia cómo deberías verificar tus artefactos.

  • No confíes solo en grep o strings. Un escaneo limpio es un filtro barato, no una verificación. Añade una prueba real de relocalización: instala el artefacto en un prefijo distinto, con un largo de ruta diferente al del build, dentro de un contenedor limpio, y corre la suite de tests ahí.
  • Si parcheas rutas in-place al instalar, como hacen Conda y Spack, verifica en cada release que la ruta siga siendo encontrable. Si el compilador la partió, tu reemplazo no tendrá nada sobre lo que actuar y el paquete fallará en la máquina del usuario.
  • Adopta verificación de builds reproducibles bit a bit en lugar de depender del escaneo de rutas, y fija el directorio de build en tu pipeline para que el proceso sea determinista.
  • Cuida el debugging. strings puede mostrarte rutas que no existen, como el xbrew del ejemplo, y hacerte perder horas persiguiendo un directorio fantasma.
  • Revisa tus banderas de compilación. Si el problema te afecta, -Os o -mstore-max=128 reducen el alcance, pero no lo eliminan: las cadenas de hasta 256 bytes siguen partiéndose.

La discusión está abierta en la lista gcc-patches, donde el autor propuso un parche para poder desactivar la optimización. Vale la pena seguirla si tu producto depende de que los binarios sean inspeccionables y relocalizables.

Una búsqueda de cobertura adicional sobre este cambio no arrojó datos verificables fuera del análisis original, así que todo lo anterior se apoya en ese texto.

Fuentes

¿te gustó o sirvió lo que leíste?, Por favor, comparte.

¿Y esto cómo se aplica en tu negocio?

Dueños de negocio de Latinoamérica y España usan un agente de IA para ganar más y recuperar su tiempo. El Kit «tu mano derecha con IA» te muestra cómo en un video de 11 minutos, con una ficha para armar el tuyo. CAR está cerrada por ahora; al anotarte, te avisamos cuando abra.

👥 Quiero el Kit →

Daily Shot: Tu ventaja táctica

Lo que pasó en las últimas 24 horas, resumido para que tú no tengas que filtrarlo.

Suscríbete para recibir cada mañana la curaduría definitiva del ecosistema startup e inversionista. Sin ruido ni rodeos, solo la información estratégica que necesitas para avanzar:

  • Venture Capital & Inversiones: Rondas, fondos y movimientos de capital.
  • IA & Tecnología: Tendencias, Web3 y herramientas de automatización.
  • Modelos de Negocio: Actualidad en SaaS, Fintech y Cripto.
  • Propósito: Erradicar el estancamiento informativo dándote claridad desde tu primer café.

📡 El Daily Shot Startupero

Noticias del ecosistema startup en 2 minutos. Gratis, todos los días.

Share to...