Description: I am reporting a reproducible silent crash in Vim 9.1 on Windows 11 using the xchacha20v2 cryptmethod. The crash occurs on an Intel i9-13900K processor immediately upon providing the correct password for a file.
Analysis: The crash produces no entry in the Windows Event Log. I have verified that:
The file is valid (it decrypts correctly using vimcrypt on Linux).
The crash persists even when CPU affinity is set to a single core.
The crash occurs in both Nightly and Stable builds.
Observations on Encrypted Header: A hex dump of a file encrypted with this build shows anomalous values for opslimit and memlimit. For example: A8 9C 8C E9 (~3.9 billion ops) 55 EC 01 F5 (~4.1 GB mem) This suggests the issue may start during the encryption/save phase, where uninitialized memory or a pointer offset is being written into the Argon2id parameter fields.
Upon providing the correct password, the decrypted file is loaded in Vim.
9.1.1825, 9.1.2006
Operating system: Microsoft Windows 11 [Version 10.0.22631.6199]
Terminal: Windows Terminal Version: 1.23.13503.0
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
What do you mean with the file is valid? If those parameters are wrong, then surely this is not valid? Then what do you mean with vimcrypt? And finally can you reproduce how this has happened? Can you share that file?
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
What do you mean with the file is valid?
The encrypted file (using xchacha20v2 in vim/Windows) can't be decrypted on the same machine, as vim crashes upon entering the correct password. I investigated the possibility that the encryption proceess, using the admittedly experimental xchacha20v2, might have corrupted something (header, content etc). I moved the file to a machine, running Debian Trixie on an old processor (i5-6200U) and the file crashed again upon entering the correct password - which reinforced my suspicion that some corruption had occured at encryption time. However, to my surprise, vimcrypt was able to decrypt the file as intended, which negated my initial hypothesis and lead me to a different one (silent exit usually means segmentation fault).
Can you share that file?
I wish I could, but it's got some really sensitive info inside.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
I can provide you with some other info you may find useful.
I ran the following script to check the magic numbers for the encrypted file:
import struct
file_path = input("Enter path to file: ")
with open(file_path, 'rb') as f:
data = f.read(64) # Just read the header area
header = data[:12]
opslimit = struct.unpack('<I', data[28:32])[0]
memlimit = struct.unpack('<I', data[32:36])[0]
print(f"Header: {header}")
print(f"Raw Opslimit: {opslimit}")
print(f"Raw Memlimit: {memlimit} (Bytes)")
print(f"Memory requested: {memlimit / 1024 / 1024:.2f} MiB")
The script extracted some really anomalous values from the encrypted file:
OpsLimit=3198306472, MemLimit=4110543957 (4 Gigabytes of RAM just to attempt to derive the key?! OpsLimit over 3 Billion?! that would take years to process on a home computer!)
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
(Continued)
I then ran another script that looked through the first 100 bytes of the encrypted file, to find where the Argon2 limits actually live by looking for "reasonable" values.
def scan_vim_header(file_path):
with open(file_path, 'rb') as f:
data = f.read(100)
print("--- Header Scan Results ---")
# We are looking for an OpsLimit (usually 1-10)
# and a MemLimit (usually 64MB to 512MB in bytes)
for i in range(12, 60):
# Read 4 bytes as a Little-Endian integer
val = struct.unpack('<I', data[i:i+4])[0]
# Check if this looks like a reasonable Argon2 MemLimit (between 1MB and 1GB)
if 1000000 < val < 1073741824:
print(f"Possible MemLimit found at offset {i}: {val} bytes ({val/1024/1024:.2f} MiB)")
# Usually OpsLimit is the 4 bytes immediately BEFORE MemLimit
ops = struct.unpack('<I', data[i-4:i])[0]
print(f"Associated OpsLimit (at offset {i-4}): {ops}")
print("-" * 20)
if name == "main":
path = input("Enter file path: ").strip()
scan_vim_header(path)
The scanner gave 9 results (possible MemLimit found at offsets 15, 24, 31, 33, 44, 49, 55, 57, 58).
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
(Continued)
Based on that output, I concentrated on offset 33, thinking that if the scanner found a result at offset 33, that was almost certainly the Memlimit, as n the standard Vim +sodium implementation:
Header: Bytes 0–11 (VimCrypt~05!)
Salt: Bytes 12–27 (16 bytes)
Opslimit: Bytes 28–31 (4 bytes)
Memlimit: Bytes 32–35 (4 bytes) — This starts at Offset 32 or 33 depending on how the scanner counts.
Based on those findings, I ran the following script:
import struct
import getpass
from nacl.secret import Aead
from nacl.pwhash import argon2id
def decrypt_vim_final(file_path, password, mem_offset=32):
with open(file_path, 'rb') as f:
data = f.read()
# Standard Vim Crypt ~05! Layout
# Header: 0-11
# Salt: 12-27
# Opslimit: 28-31
# Memlimit: 32-35 (Standard offset is 32)
# Nonce: 36-59
# Ciphertext: 60+
salt = data[12:28]
opslimit = struct.unpack('<I', data[mem_offset-4 : mem_offset])[0]
memlimit = struct.unpack('<I', data[mem_offset : mem_offset+4])[0]
nonce = data[mem_offset+4 : mem_offset+28]
ciphertext = data[mem_offset+28:]
print(f"[*] Attempting decryption with MemLimit at offset {mem_offset}...")
print(f"[*] Parameters: Ops={opslimit}, Mem={memlimit/1024/1024:.2f} MiB")
try:
key = argon2id.kdf(
Aead.KEY_SIZE,
password.encode('utf-8'),
salt,
opslimit=opslimit,
memlimit=memlimit
)
box = Aead(key)
decrypted = box.decrypt(ciphertext, None, nonce)
return decrypted.decode('utf-8')
except Exception as e:
print(f"[-] Failed at offset {mem_offset}: {e}")
return None
if name == "main":
path = input("File path: ").strip()
pwd = getpass.getpass("Password: ")
# Try the most likely offsets based on your scanner
# If 33 was your result, try 32 (0-indexed)
for offset in [32, 33]:
result = decrypt_vim_final(path, pwd, mem_offset=offset)
if result:
print("\n[SUCCESS] Content recovered:\n")
print(result)
break
This new script got stuck again, with high CPU activity. I thought the script was successfully running the Argon2id hashing algorithm, but the math was simply too heavy for the i5-6200U to complete in a reasonable timeframe.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
(Continued)
I then tried to force normal limits and ran a version of the script that ignores the file's header numbers and uses the standard defaults instead.
import struct
import getpass
from nacl.secret import Aead
from nacl.pwhash import argon2id
def bypass_decrypt(file_path, password):
with open(file_path, 'rb') as f:
data = f.read()
# We manually define the "Moderate" limits that Vim defaults to
# instead of reading the (likely misaligned) numbers from the file.
OPS_MODERATE = 3
MEM_MODERATE = 256 * 1024 * 1024 # 256 MiB
# Offsets for VimCrypt~05!
salt = data[12:28]
nonce = data[36:60]
ciphertext = data[60:]
print(f"[*] Forcing Moderate Limits (Ops: {OPS_MODERATE}, Mem: 256MB)")
print("[*] Deriving key...")
try:
# This will take < 1 second on the i5-6200U
key = argon2id.kdf(
Aead.KEY_SIZE,
password.encode('utf-8'),
salt,
opslimit=OPS_MODERATE,
memlimit=MEM_MODERATE
)
box = Aead(key)
decrypted = box.decrypt(ciphertext, None, nonce)
return decrypted.decode('utf-8')
except Exception as e:
print(f"[-] Decryption failed: {e}")
return None
if name == "main":
path = input("File path: ").strip()
pwd = getpass.getpass("Password: ")
result = bypass_decrypt(path, pwd)
if result:
print("\n[SUCCESS]\n" + result)
The script ran successfully but the result was "Decryption failed".
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
(Continued)
I then ran a script that probed the most likely byte-offsets. Instead of guessing, it tried the 4 most common structural alignments used by different versions of Vim's sodium integration.
import struct
import getpass
from nacl.secret import Aead
from nacl.pwhash import argon2id
def try_decrypt(data, password, salt_offset, ops_offset, mem_offset, nonce_offset, ct_offset):
try:
salt = data[salt_offset : salt_offset+16]
opslimit = struct.unpack('<I', data[ops_offset : ops_offset+4])[0]
memlimit = struct.unpack('<I', data[mem_offset : mem_offset+4])[0]
nonce = data[nonce_offset : nonce_offset+24]
ciphertext = data[ct_offset:]
# Sanity check: If limits are crazy, skip this offset to avoid hanging
if opslimit > 20 or memlimit > 1024*1024*1024:
return None
key = argon2id.kdf(Aead.KEY_SIZE, password.encode(), salt, opslimit, memlimit)
return Aead(key).decrypt(ciphertext, None, nonce).decode('utf-8')
except:
return None
def recover_data(path, password):
with open(path, 'rb') as f:
data = f.read()
# Define common offset patterns [Salt, Ops, Mem, Nonce, CipherText]
patterns = [
[12, 28, 32, 36, 60], # Standard (Vim source default)
[12, 32, 36, 40, 64], # Padded / 64-bit aligned
[16, 32, 36, 40, 64], # Shifted (VimCrypt~05! + 4 padding)
[12, 29, 33, 37, 61], # The "Offset 33" pattern your scanner found
]
for i, p in enumerate(patterns):
print(f"[*] Trying pattern {i+1}...")
result = try_decrypt(data, password, *p)
if result:
return result
return None
if name == "main":
p = input("File path: ").strip()
pwd = getpass.getpass("Password: ")
final_text = recover_data(p, pwd)
if final_text:
print("\n[SUCCESS] Content recovered:\n" + final_text)
else:
print("\n[!] All standard patterns failed. My Vim build may use unique padding.")
Unfortunately, the output was: "All standard patterns failed. My Vim build may use unique padding."
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
(Continued)
I then hex-dumped the first bytes of the encrypted file:
56 69 6D 43 72 79 70 74 7E 30 35 21 78 14 2A 7F FF 69 09 7B 48 F5 41 D1 88 FA 71 2E A8 9C 8C E9 55 EC 01 F5 02 00 00 00 00 00 00 [...]
Meaning:
56 69 6D 43 72 79 70 74 7E 30 35 21 Header "VimCrypt~05!"
78 14 2A 7F FF 69 09 7B 48 F5 41 D1 88 FA 71 2E Salt
A8 9C 8C E9 Opslimit 3,918,273,704 (Decimal)
55 EC 01 F5 Memlimit 4,110,543,957 (Decimal)
Again, the numbers stored for Opslimit and Memlimit are astronomical.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
(Continued)
Let's run a script that ignores the crazy numbers in the encrypted file and forces the Standard Moderate Limits that Vim is supposed to use.
import struct
import getpass
from nacl.secret import Aead
from nacl.pwhash import argon2id
def final_rescue(file_path, password):
with open(file_path, 'rb') as f:
data = f.read()
# We manually extract only the Salt and Nonce,
# ignoring the 'garbage' limits at bytes 28-35.
salt = data[12:28]
# In the hex dump, the Nonce starts after the 8 bytes of garbage limits
nonce = data[36:60]
ciphertext = data[60:]
# Force standard Vim defaults (Moderate)
OPS = 3
MEM = 256 * 1024 * 1024 # 256 MiB
print(f"[*] Bypassing corrupted header limits...")
print(f"[*] Forcing Ops={OPS}, Mem={MEM/1024/1024}MB")
try:
key = argon2id.kdf(Aead.KEY_SIZE, password.encode(), salt, opslimit=OPS, memlimit=MEM)
box = Aead(key)
decrypted = box.decrypt(ciphertext, None, nonce)
return decrypted.decode('utf-8')
except Exception as e:
print(f"[-] Recovery failed: {e}")
return None
if name == "main":
path = input("Enter path to file: ").strip()
pwd = getpass.getpass("Enter password: ")
content = final_rescue(path, pwd)
if content:
print("\n[SUCCESS] DATA RECOVERED:\n" + content)
Again: "Recovery failed. Decryption failed"
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
(Continued)
At this point, the file content seemed irretrievable. But then, in a "Hail Mary" attempt, I ran vimcrypt and... the file was decrypted as expected!
Could the fact that I was running a nightly build be the culprit? let's switch to the stable version and try the encryption/decryption with it.
The stable version exhibited the same behavior trying to decrypt the encrypted file (i.e. instant crash, SefFault or Access violation - impossible to tell, as there was nothing written into the Event Log). Could it be the interaction between my i9-13900K and libsodium (SIGILL, libsodium attempts to use a specific instruction my Windows build/configuration doesn't like, and the CPU generates an Illegal Instruction signal?) Memory Protection (DEP/ASLR) (Vim's internal implementation tries to allocate this memory in a way that violates Data Execution Prevention (DEP), Windows kills the process silently)? Hybrid architecture" hiccup (of Windows tries to shift the Argon2 thread from a P-core to an E-core mid-calculation and the libsodium library isn't "thread-safe" for that specific jump the instruction pointer hits a wall)?
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
(Continued)
I then ran another experiment: I ran vim setting its affinity to a single core, then tried to open the file previously encrypted with xchacha20v2. The same crash occured.
I interpreted that as: the crash isn't caused by a race condition or a multi-core synchronization error. Since it still fails even on a single core, we are dealing with a Direct Execution Failure within the libsodium library's interaction with the i9's architecture.
The lack of an Event Log entry combined with the silent exit points to a Hardware exception (Access violation) that is killing the process before Windows can even wrap it in an error report. (SIMD/AVX Instruction Set Mismatch? Memory Alignment/Guard Pages?)
At that point, I stopped experimenting/investigating. Hope the information above proves useful to you
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
sorry, I don't follow, your messages are too chaotic and too much python scripts, that I don't know how it relates. Please give a concise summary of what you found out.
Please answer the following questions from my initial comment:
There haven't been any changes in the vim crypt implementation lately (for at least 2 years), so it is not a difference between vim nightly and stable builds.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
* Again, what is vimcrypt?
https://github.com/chrisbra/vimcrypt
* how did you create that file and with what vim version?
Standard text file, UTF-8, LF. Created as unencrypted file, initially.
* can you reproduce the issue? If yes how?
Yes. Change default encryption method from blowfish2 to xchacha20v2. Save file (previously encrypted with blowfish2) as a xchacha20v2 encrypted file. Experience crash.
* can you share the file?
As I was saying in my very first reply, no, sorry. Private/confidential info.
Sorry you're finding my investigative logic "chaotic". I thought I documented each step/script result with a conclusion.
Feel free to close this incident report if you want.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
Oh my vimcrypt? Hm, let me try to reproduce.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()