Skip to content

Commit c1e92af

Browse files
committed
fix: close issue #249 — Emscripten OOM in CKKS unit tests
PR #1179 added ClearBootstrapPrecom and ClearSchemeSwitchPrecom helpers but did not wire them into ReleaseAllContexts. This change completes that work, then re-enables the EVAL_FAST_ROTATION and CONTEXT_WITH_SERTYPE tests that PR #985 had to guard under !defined(__EMSCRIPTEN__) for the original std::bad_alloc failure. Library changes (no hot-path touched): - ReleaseAllContexts now walks every live context and calls ClearAllCKKSCaches before clearing the static maps. - Add ReleaseAllContextsAndTrim and TrimAllocator (malloc_trim on glibc, malloc_zone_pressure_relief on Apple, _heapmin on MSVC, no-op else). - Add ClearAllCKKSCaches convenience on CryptoContextImpl. - ClearBootstrapPrecom and ClearSchemeSwitchPrecom bodies made noexcept-safe so the walk does not throw on contexts that have not Enable(FHE)'d or Enable(SCHEMESWITCH)'d. Test changes: - TearDown in UTCKKSrns and UTCKKSrnsSerialize now calls ReleaseAllContextsAndTrim. - Mid-test ClearAllCKKSCaches between JSON and BINARY rounds in UnitTestCKKSrnsSerialize. - UTCKKSCacheClear extends its WITH_TCM skip to __EMSCRIPTEN__ (the heap-probe helper has no emmalloc backend). - Remove the !defined(__EMSCRIPTEN__) guards around the affected rows. CI: - Add .github/workflows/emscripten.yml: builds with emsdk 3.1.59 and runs pke_tests/core_tests under node in two slices. Validated on macOS + WASM: - Native: pke_tests 1892/1892 pass, core_tests 158/158 pass. - WASM: CKKS slice 939/941 pass (2 heap-probe tests skipped), BFV+BGV+binfhe slice 324/324 pass, peak 1.6 GB. Closes #249.
1 parent 67334e2 commit c1e92af

9 files changed

Lines changed: 140 additions & 22 deletions

File tree

.github/workflows/emscripten.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Emscripten
2+
3+
# Builds with emcc and runs the WASM unit tests under node. Guards against
4+
# re-introducing the cache-leak class that issue #249 tracked.
5+
6+
on:
7+
pull_request:
8+
branches: [dev]
9+
push:
10+
branches: [dev, main]
11+
workflow_dispatch:
12+
13+
jobs:
14+
build-and-test:
15+
runs-on: ubuntu-22.04
16+
timeout-minutes: 60
17+
steps:
18+
- uses: actions/checkout@v5
19+
- uses: actions/setup-node@v5
20+
with: { node-version: '20' }
21+
22+
- name: Cache emsdk
23+
id: cache-emsdk
24+
uses: actions/cache@v4
25+
with:
26+
path: ~/emsdk
27+
key: emsdk-3.1.59
28+
29+
- name: Install emsdk
30+
if: steps.cache-emsdk.outputs.cache-hit != 'true'
31+
run: |
32+
git clone --depth 1 --branch 3.1.59 https://github.com/emscripten-core/emsdk.git ~/emsdk
33+
~/emsdk/emsdk install 3.1.59
34+
~/emsdk/emsdk activate 3.1.59
35+
36+
- name: Configure
37+
run: |
38+
source ~/emsdk/emsdk_env.sh
39+
emcmake cmake -S . -B build \
40+
-DCMAKE_BUILD_TYPE=Release -DBUILD_UNITTESTS=ON \
41+
-DBUILD_BENCHMARKS=OFF -DBUILD_EXAMPLES=OFF -DBUILD_EXTRAS=OFF \
42+
-DBUILD_STATIC=ON -DBUILD_SHARED=OFF -DWITH_OPENMP=OFF -DNATIVE_SIZE=64 \
43+
-DCMAKE_EXE_LINKER_FLAGS="-s INITIAL_MEMORY=536870912 -s ALLOW_MEMORY_GROWTH=1 -s MAXIMUM_MEMORY=4294967296 -s WASM=1 -s NODERAWFS=1 -s STACK_SIZE=5MB -s ASSERTIONS=0"
44+
45+
- name: Build
46+
run: |
47+
source ~/emsdk/emsdk_env.sh
48+
cmake --build build --target core_tests pke_tests -j2
49+
50+
- name: core_tests
51+
working-directory: build/unittest
52+
run: node --max-old-space-size=8192 ./core_tests.js --gtest_color=no
53+
54+
# Split pke_tests across two processes — each stays well below the 4 GB WASM cap.
55+
- name: pke_tests (CKKS + general)
56+
working-directory: build/unittest
57+
run: node --max-old-space-size=8192 ./pke_tests.js --gtest_filter='UTCKKSRNS*:UTCKKSCacheClear*:UTGENERAL*' --gtest_color=no
58+
59+
- name: pke_tests (BFV + BGV + binfhe)
60+
working-directory: build/unittest
61+
run: node --max-old-space-size=8192 ./pke_tests.js --gtest_filter='UTBFV*:UTBGV*:UTBINFHE*' --gtest_color=no

src/core/include/utils/memory.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ void MoveAppend(std::vector<X>& dst, std::vector<X>& src) {
6363
*/
6464
void secure_memset(volatile void* mem, uint8_t c, size_t len);
6565

66+
/// Trim/purge the C allocator so freed pages return to the OS. Returns true if
67+
/// the platform has a trim primitive, false (no-op) otherwise.
68+
bool TrimAllocator();
69+
6670
} // namespace lbcrypto
6771

6872
#endif // LBCRYPTO_UTILS_MEMORY_H

src/core/lib/utils/memory.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@
3030
//==================================================================================
3131
#include "utils/memory.h"
3232

33+
#if defined(__APPLE__)
34+
#include <mach/mach.h>
35+
#include <malloc/malloc.h>
36+
#elif defined(__GLIBC__)
37+
#include <malloc.h>
38+
#elif defined(_MSC_VER)
39+
#include <malloc.h>
40+
#endif
41+
3342
namespace lbcrypto {
3443

3544
void secure_memset(volatile void* mem, uint8_t c, size_t len) {
@@ -38,4 +47,27 @@ void secure_memset(volatile void* mem, uint8_t c, size_t len) {
3847
*(ptr + i) = c;
3948
}
4049

50+
bool TrimAllocator() {
51+
#if defined(__GLIBC__)
52+
malloc_trim(0);
53+
return true;
54+
#elif defined(__APPLE__)
55+
vm_address_t* zones = nullptr;
56+
unsigned count = 0;
57+
if (malloc_get_all_zones(mach_task_self(), nullptr, &zones, &count) == KERN_SUCCESS && zones) {
58+
for (unsigned i = 0; i < count; ++i) {
59+
auto* z = reinterpret_cast<malloc_zone_t*>(zones[i]);
60+
if (z)
61+
malloc_zone_pressure_relief(z, 0);
62+
}
63+
}
64+
return true;
65+
#elif defined(_MSC_VER)
66+
_heapmin();
67+
return true;
68+
#else
69+
return false;
70+
#endif
71+
}
72+
4173
} // namespace lbcrypto

src/pke/include/cryptocontext.h

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -622,20 +622,22 @@ class CryptoContextImpl : public Serializable {
622622
*/
623623
static void ClearStaticMapsAndVectors();
624624

625-
/**
626-
* @brief Clear CKKS bootstrap precomputations cached by this context's scheme.
627-
*/
625+
/// Clear CKKS bootstrap precomputations. Safe on any scheme.
628626
void ClearBootstrapPrecom() noexcept {
629-
VerifyCKKSScheme(__func__);
630-
m_scheme->ClearBootstrapPrecom();
627+
if (m_scheme)
628+
m_scheme->ClearBootstrapPrecom();
631629
}
632630

633-
/**
634-
* @brief Clear CKKS/FHEW scheme-switch precomputations cached by this context's scheme.
635-
*/
631+
/// Clear CKKS/FHEW scheme-switch precomputations. Safe on any scheme.
636632
void ClearSchemeSwitchPrecom() noexcept {
637-
VerifyCKKSScheme(__func__);
638-
m_scheme->ClearSchemeSwitchPrecom();
633+
if (m_scheme)
634+
m_scheme->ClearSchemeSwitchPrecom();
635+
}
636+
637+
/// Release all scheme-level caches (bootstrap + scheme-switch).
638+
void ClearAllCKKSCaches() noexcept {
639+
ClearBootstrapPrecom();
640+
ClearSchemeSwitchPrecom();
639641
}
640642

641643
/**

src/pke/include/cryptocontextfactory.h

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include "cryptocontext-fwd.h"
3636
#include "lattice/lat-hal.h"
3737
#include "scheme/scheme-id.h"
38+
#include "utils/memory.h"
3839

3940
#include <memory>
4041
#include <string>
@@ -63,12 +64,25 @@ class CryptoContextFactory {
6364

6465
public:
6566
static void ReleaseAllContexts() {
66-
if (!AllContexts.empty())
67+
// Drop scheme-level caches on every live context so memory held by
68+
// m_bootPrecomMap / scheme-switch state is freed even if callers
69+
// still hold a CryptoContext (issue #533).
70+
for (auto& cc : AllContexts) {
71+
if (cc)
72+
cc->ClearAllCKKSCaches();
73+
}
74+
if (AllContexts.size() > 0)
6775
AllContexts[0]->ClearStaticMapsAndVectors();
6876

6977
AllContexts.clear();
7078
}
7179

80+
/// Like ReleaseAllContexts() but also trims the C allocator so RSS drops.
81+
static void ReleaseAllContextsAndTrim() {
82+
ReleaseAllContexts();
83+
TrimAllocator();
84+
}
85+
7286
static int GetContextCount() {
7387
return AllContexts.size();
7488
}

src/pke/include/schemebase/base-scheme.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,13 +1147,13 @@ class SchemeBase {
11471147
}
11481148

11491149
void ClearBootstrapPrecom() noexcept {
1150-
VerifyFHEEnabled(__func__);
1151-
m_FHE->ClearBootstrapPrecom();
1150+
if (m_FHE)
1151+
m_FHE->ClearBootstrapPrecom();
11521152
}
11531153

11541154
void ClearSchemeSwitchPrecom() noexcept {
1155-
VerifySchemeSwitchEnabled(__func__);
1156-
m_SchemeSwitch->ClearSchemeSwitchPrecom();
1155+
if (m_SchemeSwitch)
1156+
m_SchemeSwitch->ClearSchemeSwitchPrecom();
11571157
}
11581158

11591159
std::shared_ptr<std::map<uint32_t, EvalKey<Element>>> EvalBootstrapKeyGen(const PrivateKey<Element> privateKey,

src/pke/unittest/utckksrns/UnitTestCKKSCacheClear.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,13 @@ CryptoContext<DCRTPoly> MakeSchemeSwitchCC() {
116116

117117
class UTCKKSCacheClear : public ::testing::Test {
118118
protected:
119-
#if defined(WITH_TCM)
119+
#if defined(WITH_TCM) || defined(__EMSCRIPTEN__)
120120
void SetUp() override {
121+
#if defined(WITH_TCM)
121122
GTEST_SKIP() << "Heap usage checks are not stable with tcmalloc enabled";
123+
#else
124+
GTEST_SKIP() << "Heap probe unavailable under Emscripten";
125+
#endif
122126
}
123127
#endif
124128

src/pke/unittest/utckksrns/UnitTestCKKSrns.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,8 @@ static std::vector<TEST_CASE_UTCKKSRNS> testCases = {
398398
{ EVAL_FAST_ROTATION, "39", {CKKSRNS_SCHEME, RING_DIM, 7, DFLT, DSIZE, BATCH, DFLT, DFLT, DFLT, HEStd_NotSet, BV, FLEXIBLEAUTOEXT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT}, BATCH},
399399
{ EVAL_FAST_ROTATION, "40", {CKKSRNS_SCHEME, RING_DIM, 7, DFLT, DSIZE, BATCH, DFLT, DFLT, DFLT, HEStd_NotSet, HYBRID, FLEXIBLEAUTOEXT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT}, BATCH},
400400
#endif
401-
#if !defined(__EMSCRIPTEN__)
401+
// Issue #249: previously gated under !defined(__EMSCRIPTEN__) — re-enabled
402+
// now that TearDown calls ReleaseAllContextsAndTrim (PR #1179).
402403
{ EVAL_FAST_ROTATION, "41", {CKKSRNS_SCHEME, RING_DIM, 7, DFLT, DSIZE, BATCH, DFLT, DFLT, DFLT, HEStd_NotSet, BV, FIXEDMANUAL, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT}, RING_DIM_HALF},
403404
{ EVAL_FAST_ROTATION, "42", {CKKSRNS_SCHEME, RING_DIM, 7, DFLT, DSIZE, BATCH, DFLT, DFLT, DFLT, HEStd_NotSet, BV, FIXEDAUTO, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT}, RING_DIM_HALF},
404405
{ EVAL_FAST_ROTATION, "43", {CKKSRNS_SCHEME, RING_DIM, 7, DFLT, DSIZE, BATCH, DFLT, DFLT, DFLT, HEStd_NotSet, HYBRID, FIXEDMANUAL, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT}, RING_DIM_HALF},
@@ -527,7 +528,6 @@ static std::vector<TEST_CASE_UTCKKSRNS> testCases = {
527528
// TestType, Descr, Scheme, RDim, MultDepth, SModSize, DSize, BatchSz, SecKeyDist, MaxRelinSkDeg, FModSize, SecLvl, KSTech, ScalTech, LDigits, PtMod, StdDev, EvalAddCt, KSCt, MultTech, EncTech, PREMode
528529
{ SMALL_SCALING_MOD_SIZE, "01", {CKKSRNS_SCHEME, 32768, 19, 22, DFLT, DFLT, DFLT, DFLT, 23, DFLT, DFLT, FIXEDMANUAL, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT}, },
529530
{ SMALL_SCALING_MOD_SIZE, "02", {CKKSRNS_SCHEME, 32768, 16, 50, DFLT, DFLT, DFLT, DFLT, 50, HEStd_NotSet, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT}, },
530-
#endif
531531
// ==========================================
532532
// TestType, Descr, Scheme, RDim, MultDepth, SModSize, DSize, BatchSz, SecKeyDist, MaxRelinSkDeg, FModSize, SecLvl, KSTech, ScalTech, LDigits, PtMod, StdDev, EvalAddCt, KSCt, MultTech, EncTech, PREMode, MultipartyMode, decryptionNoiseMode, ExecutionMode, NoiseEstimate, RegisterWordSize, compositeDegree, CKKSDataType
533533
{ EVALCOMPLEX, "01", {CKKSRNS_SCHEME, RING_DIM, 7, DFLT, DSIZE, BATCH, DFLT, DFLT, DFLT, HEStd_NotSet, BV, FIXEDMANUAL, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, COMPLEX}, },
@@ -641,7 +641,8 @@ class UTCKKSRNS : public ::testing::TestWithParam<TEST_CASE_UTCKKSRNS> {
641641
}
642642

643643
void TearDown() {
644-
CryptoContextFactory<DCRTPoly>::ReleaseAllContexts();
644+
// Trim variant keeps peak WASM linear memory under the 4 GB cap (issue #249).
645+
CryptoContextFactory<DCRTPoly>::ReleaseAllContextsAndTrim();
645646
OpenFHEParallelControls.UnitTestStop();
646647
}
647648

src/pke/unittest/utckksrns/UnitTestCKKSrnsSerialize.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ constexpr uint32_t BATCH = 16;
119119
// clang-format off
120120
static std::vector<TEST_CASE_UTCKKSRNS_SER> testCases = {
121121
// TestType, Descr, Scheme, RDim, MultDepth, SModSize, DSize, BatchSz, SecKeyDist, MaxRelinSkDeg, FModSize, SecLvl, KSTech, ScalTech, LDigits, PtMod, StdDev, EvalAddCt, KSCt, MultTech, EncTech, PREMode
122-
#if !defined(__EMSCRIPTEN__)
122+
// Issue #249: re-enabled — see matching note in UnitTestCKKSrns.cpp.
123123
{ CONTEXT_WITH_SERTYPE, "01", {CKKSRNS_SCHEME, RING_DIM, MULT_DEPTH, SMODSIZE, DSIZE, BATCH, DFLT, DFLT, DFLT, HEStd_NotSet, BV, FIXEDMANUAL, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT}, },
124124
{ CONTEXT_WITH_SERTYPE, "02", {CKKSRNS_SCHEME, RING_DIM, MULT_DEPTH, SMODSIZE, DSIZE, BATCH, DFLT, DFLT, DFLT, HEStd_NotSet, BV, FIXEDAUTO, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT}, },
125125
{ CONTEXT_WITH_SERTYPE, "03", {CKKSRNS_SCHEME, RING_DIM, MULT_DEPTH, SMODSIZE, DSIZE, BATCH, DFLT, DFLT, DFLT, HEStd_NotSet, HYBRID, FIXEDMANUAL, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT}, },
@@ -129,7 +129,6 @@ static std::vector<TEST_CASE_UTCKKSRNS_SER> testCases = {
129129
{ CONTEXT_WITH_SERTYPE, "06", {CKKSRNS_SCHEME, RING_DIM, MULT_DEPTH, SMODSIZE, DSIZE, BATCH, DFLT, DFLT, DFLT, HEStd_NotSet, HYBRID, FLEXIBLEAUTO, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT}, },
130130
{ CONTEXT_WITH_SERTYPE, "07", {CKKSRNS_SCHEME, RING_DIM, MULT_DEPTH, SMODSIZE, DSIZE, BATCH, DFLT, DFLT, DFLT, HEStd_NotSet, BV, FLEXIBLEAUTOEXT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT}, },
131131
{ CONTEXT_WITH_SERTYPE, "08", {CKKSRNS_SCHEME, RING_DIM, MULT_DEPTH, SMODSIZE, DSIZE, BATCH, DFLT, DFLT, DFLT, HEStd_NotSet, HYBRID, FLEXIBLEAUTOEXT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT, DFLT}, },
132-
#endif
133132
#endif
134133
// ==========================================
135134
// TestType, Descr, Scheme, RDim, MultDepth, SModSize, DSize, BatchSz, SecKeyDist, MaxRelinSkDeg, FModSize, SecLvl, KSTech, ScalTech, LDigits, PtMod, StdDev, EvalAddCt, KSCt, MultTech, EncTech, PREMode
@@ -181,14 +180,15 @@ class UTCKKSRNS_SER : public ::testing::TestWithParam<TEST_CASE_UTCKKSRNS_SER> {
181180
}
182181

183182
void TearDown() {
184-
CryptoContextFactory<DCRTPoly>::ReleaseAllContexts();
183+
CryptoContextFactory<DCRTPoly>::ReleaseAllContextsAndTrim(); // issue #249
185184
OpenFHEParallelControls.UnitTestStop();
186185
}
187186

188187
void UnitTestContext(const TEST_CASE_UTCKKSRNS_SER& testData, const std::string& failmsg = std::string()) {
189188
CryptoContext<Element> cc(UnitTestGenerateContext(testData.params));
190189

191190
UnitTestContextWithSertype(cc, SerType::JSON, "json");
191+
cc->ClearAllCKKSCaches(); // free caches between rounds (issue #249)
192192
UnitTestContextWithSertype(cc, SerType::BINARY, "binary");
193193
}
194194

0 commit comments

Comments
 (0)