Skip to content

Commit 73ea9f3

Browse files
committed
feat(security): own EVP_CIPHER_CTX via unique_ptr in HybridCipher base
Three subclasses (CCMCipher, ChaCha20Cipher, ChaCha20Poly1305Cipher) implemented destructors as `~SubClass() { ctx = nullptr; }` — they nulled the parent's raw pointer without freeing it first, expecting the parent destructor to do the cleanup. The parent then saw `ctx == nullptr` and skipped the EVP_CIPHER_CTX_free call entirely. Result: every cipher object of those three types leaked its OpenSSL context. Move ownership into the base class with a unique_ptr that has the EVP_CIPHER_CTX_free deleter baked in. Subclasses can no longer mismanage ctx because the destruction order (subclass → base) makes the parent's unique_ptr destructor run automatically and free the context exactly once. The buggy three subclass destructors are removed (defaulted), and XSalsa20Cipher's destructor drops its now- redundant `ctx = nullptr` line while keeping the secureZero calls from Phase 1.2. Internally every `EVP_*Foo*(ctx, ...)` OpenSSL call now uses `ctx.get()`; every `ctx = EVP_CIPHER_CTX_new()` becomes `ctx.reset(EVP_CIPHER_CTX_new())`; every manual cleanup of `EVP_CIPHER_CTX_free(ctx); ctx = nullptr;` collapses to `ctx.reset()`. Touched files: HybridCipher, CCMCipher, ChaCha20Cipher, ChaCha20Poly1305Cipher, GCMCipher, OCBCipher, XSalsa20Cipher. No new tests — the existing AEAD round-trip tests exercise both the constructor (ctx.reset(EVP_CIPHER_CTX_new())) and the destructor (unique_ptr cleanup) in every test that uses these ciphers. Phase 1.3 of plans/todo/security-audit.md.
1 parent 2fae6bd commit 73ea9f3

11 files changed

Lines changed: 86 additions & 107 deletions

packages/react-native-quick-crypto/cpp/cipher/CCMCipher.cpp

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ void CCMCipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::s
2222
size_t iv_len = native_iv->size();
2323

2424
// Set the IV length using CCM-specific control
25-
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_SET_IVLEN, iv_len, nullptr) != 1) {
25+
if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_CCM_SET_IVLEN, iv_len, nullptr) != 1) {
2626
unsigned long err = ERR_get_error();
2727
char err_buf[256];
2828
ERR_error_string_n(err, err_buf, sizeof(err_buf));
@@ -31,7 +31,7 @@ void CCMCipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::s
3131

3232
// Set the expected/output tag length using CCM-specific control.
3333
// auth_tag_len should have been defaulted or set via setArgs in the base init.
34-
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_SET_TAG, auth_tag_len, nullptr) != 1) {
34+
if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_CCM_SET_TAG, auth_tag_len, nullptr) != 1) {
3535
unsigned long err = ERR_get_error();
3636
char err_buf[256];
3737
ERR_error_string_n(err, err_buf, sizeof(err_buf));
@@ -44,7 +44,7 @@ void CCMCipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::s
4444
const unsigned char* iv_ptr = reinterpret_cast<const unsigned char*>(native_iv->data());
4545

4646
// The last argument (is_cipher) should be consistent with the initial setup call.
47-
if (EVP_CipherInit_ex(ctx, nullptr, nullptr, key_ptr, iv_ptr, is_cipher) != 1) {
47+
if (EVP_CipherInit_ex(ctx.get(), nullptr, nullptr, key_ptr, iv_ptr, is_cipher) != 1) {
4848
unsigned long err = ERR_get_error();
4949
char err_buf[256];
5050
ERR_error_string_n(err, err_buf, sizeof(err_buf));
@@ -66,7 +66,7 @@ std::shared_ptr<ArrayBuffer> CCMCipher::update(const std::shared_ptr<ArrayBuffer
6666
maybePassAuthTagToOpenSSL();
6767
}
6868

69-
int block_size = EVP_CIPHER_CTX_block_size(ctx);
69+
int block_size = EVP_CIPHER_CTX_block_size(ctx.get());
7070
if (block_size <= 0) {
7171
throw std::runtime_error("Invalid block size in update");
7272
}
@@ -79,7 +79,7 @@ std::shared_ptr<ArrayBuffer> CCMCipher::update(const std::shared_ptr<ArrayBuffer
7979
const uint8_t* in = reinterpret_cast<const uint8_t*>(native_data->data());
8080

8181
int actual_out_len = 0;
82-
int ret = EVP_CipherUpdate(ctx, out_buf.get(), &actual_out_len, in, in_len);
82+
int ret = EVP_CipherUpdate(ctx.get(), out_buf.get(), &actual_out_len, in, in_len);
8383

8484
if (!is_cipher) {
8585
// Decryption: Check for tag verification failure
@@ -115,14 +115,14 @@ std::shared_ptr<ArrayBuffer> CCMCipher::final() {
115115
}
116116

117117
// Proceed only for encryption
118-
int block_size = EVP_CIPHER_CTX_block_size(ctx);
118+
int block_size = EVP_CIPHER_CTX_block_size(ctx.get());
119119
if (block_size <= 0) {
120120
throw std::runtime_error("Invalid block size");
121121
}
122122
auto out_buf = std::make_unique<unsigned char[]>(block_size);
123123
int out_len = 0;
124124

125-
if (!EVP_CipherFinal_ex(ctx, out_buf.get(), &out_len)) {
125+
if (!EVP_CipherFinal_ex(ctx.get(), out_buf.get(), &out_len)) {
126126
unsigned long err = ERR_get_error();
127127
char err_buf[256];
128128
ERR_error_string_n(err, err_buf, sizeof(err_buf));
@@ -133,7 +133,7 @@ std::shared_ptr<ArrayBuffer> CCMCipher::final() {
133133
auth_tag_len = sizeof(auth_tag);
134134
}
135135

136-
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_GET_TAG, auth_tag_len, auth_tag) != 1) {
136+
if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_CCM_GET_TAG, auth_tag_len, auth_tag) != 1) {
137137
unsigned long err = ERR_get_error();
138138
char err_buf[256];
139139
ERR_error_string_n(err, err_buf, sizeof(err_buf));
@@ -179,7 +179,7 @@ bool CCMCipher::setAAD(const std::shared_ptr<ArrayBuffer>& data, std::optional<d
179179
// BUT the wiki says "(only needed if AAD is passed)". Let's skip if decrypting and AAD length is 0.
180180
bool should_set_total_length = is_cipher || aad_len > 0;
181181
if (should_set_total_length) {
182-
if (EVP_CipherUpdate(ctx, nullptr, &out_len, nullptr, data_len) != 1) {
182+
if (EVP_CipherUpdate(ctx.get(), nullptr, &out_len, nullptr, data_len) != 1) {
183183
unsigned long err = ERR_get_error();
184184
char err_buf[256];
185185
ERR_error_string_n(err, err_buf, sizeof(err_buf));
@@ -190,7 +190,7 @@ bool CCMCipher::setAAD(const std::shared_ptr<ArrayBuffer>& data, std::optional<d
190190
// 2. Process AAD Data
191191
// Per OpenSSL CCM decryption examples, this MUST be called even if aad_len is 0.
192192
// Pass nullptr as the output buffer, the AAD data pointer, and its length.
193-
if (EVP_CipherUpdate(ctx, nullptr, &out_len, native_aad->data(), aad_len) != 1) {
193+
if (EVP_CipherUpdate(ctx.get(), nullptr, &out_len, native_aad->data(), aad_len) != 1) {
194194
unsigned long err = ERR_get_error();
195195
char err_buf[256];
196196
ERR_error_string_n(err, err_buf, sizeof(err_buf));

packages/react-native-quick-crypto/cpp/cipher/CCMCipher.hpp

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@ namespace margelo::nitro::crypto {
77
class CCMCipher : public HybridCipher {
88
public:
99
CCMCipher() : HybridObject(TAG) {}
10-
~CCMCipher() {
11-
// Let parent destructor free the context
12-
ctx = nullptr;
13-
}
10+
// Destructor defaulted: HybridCipher's unique_ptr ctx frees itself.
11+
~CCMCipher() override = default;
1412

1513
void init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::shared_ptr<ArrayBuffer> iv) override;
1614
std::shared_ptr<ArrayBuffer> update(const std::shared_ptr<ArrayBuffer>& data) override;

packages/react-native-quick-crypto/cpp/cipher/ChaCha20Cipher.cpp

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,8 @@
77
namespace margelo::nitro::crypto {
88

99
void ChaCha20Cipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::shared_ptr<ArrayBuffer> iv) {
10-
// Clean up any existing context
11-
if (ctx) {
12-
EVP_CIPHER_CTX_free(ctx);
13-
ctx = nullptr;
14-
}
10+
// Resetting the unique_ptr frees any previous context.
11+
ctx.reset();
1512

1613
// Get ChaCha20 cipher implementation
1714
const EVP_CIPHER* cipher = EVP_chacha20();
@@ -20,18 +17,17 @@ void ChaCha20Cipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const s
2017
}
2118

2219
// Create a new context
23-
ctx = EVP_CIPHER_CTX_new();
20+
ctx.reset(EVP_CIPHER_CTX_new());
2421
if (!ctx) {
2522
throw std::runtime_error("Failed to create cipher context");
2623
}
2724

2825
// Initialize the encryption/decryption operation
29-
if (EVP_CipherInit_ex(ctx, cipher, nullptr, nullptr, nullptr, is_cipher) != 1) {
26+
if (EVP_CipherInit_ex(ctx.get(), cipher, nullptr, nullptr, nullptr, is_cipher) != 1) {
3027
unsigned long err = ERR_get_error();
3128
char err_buf[256];
3229
ERR_error_string_n(err, err_buf, sizeof(err_buf));
33-
EVP_CIPHER_CTX_free(ctx);
34-
ctx = nullptr;
30+
ctx.reset();
3531
throw std::runtime_error("ChaCha20Cipher: Failed initial CipherInit setup: " + std::string(err_buf));
3632
}
3733

@@ -52,12 +48,11 @@ void ChaCha20Cipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const s
5248
const unsigned char* key_ptr = reinterpret_cast<const unsigned char*>(native_key->data());
5349
const unsigned char* iv_ptr = reinterpret_cast<const unsigned char*>(native_iv->data());
5450

55-
if (EVP_CipherInit_ex(ctx, nullptr, nullptr, key_ptr, iv_ptr, is_cipher) != 1) {
51+
if (EVP_CipherInit_ex(ctx.get(), nullptr, nullptr, key_ptr, iv_ptr, is_cipher) != 1) {
5652
unsigned long err = ERR_get_error();
5753
char err_buf[256];
5854
ERR_error_string_n(err, err_buf, sizeof(err_buf));
59-
EVP_CIPHER_CTX_free(ctx);
60-
ctx = nullptr;
55+
ctx.reset();
6156
throw std::runtime_error("ChaCha20Cipher: Failed to set key/IV: " + std::string(err_buf));
6257
}
6358
}
@@ -76,7 +71,7 @@ std::shared_ptr<ArrayBuffer> ChaCha20Cipher::update(const std::shared_ptr<ArrayB
7671
uint8_t* out = new uint8_t[out_len];
7772

7873
// Perform the cipher update operation
79-
if (EVP_CipherUpdate(ctx, out, &out_len, native_data->data(), in_len) != 1) {
74+
if (EVP_CipherUpdate(ctx.get(), out, &out_len, native_data->data(), in_len) != 1) {
8075
delete[] out;
8176
unsigned long err = ERR_get_error();
8277
char err_buf[256];

packages/react-native-quick-crypto/cpp/cipher/ChaCha20Cipher.hpp

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@ namespace margelo::nitro::crypto {
77
class ChaCha20Cipher : public HybridCipher {
88
public:
99
ChaCha20Cipher() : HybridObject(TAG) {}
10-
~ChaCha20Cipher() {
11-
// Let parent destructor free the context
12-
ctx = nullptr;
13-
}
10+
// Destructor defaulted: HybridCipher's unique_ptr ctx frees itself.
11+
~ChaCha20Cipher() override = default;
1412

1513
void init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::shared_ptr<ArrayBuffer> iv) override;
1614
std::shared_ptr<ArrayBuffer> update(const std::shared_ptr<ArrayBuffer>& data) override;

packages/react-native-quick-crypto/cpp/cipher/ChaCha20Poly1305Cipher.cpp

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,8 @@
77
namespace margelo::nitro::crypto {
88

99
void ChaCha20Poly1305Cipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::shared_ptr<ArrayBuffer> iv) {
10-
// Clean up any existing context
11-
if (ctx) {
12-
EVP_CIPHER_CTX_free(ctx);
13-
ctx = nullptr;
14-
}
10+
// Resetting the unique_ptr frees any previous context.
11+
ctx.reset();
1512

1613
// Get ChaCha20-Poly1305 cipher implementation
1714
const EVP_CIPHER* cipher = EVP_chacha20_poly1305();
@@ -20,18 +17,17 @@ void ChaCha20Poly1305Cipher::init(const std::shared_ptr<ArrayBuffer> cipher_key,
2017
}
2118

2219
// Create a new context
23-
ctx = EVP_CIPHER_CTX_new();
20+
ctx.reset(EVP_CIPHER_CTX_new());
2421
if (!ctx) {
2522
throw std::runtime_error("Failed to create cipher context");
2623
}
2724

2825
// Initialize the encryption/decryption operation
29-
if (EVP_CipherInit_ex(ctx, cipher, nullptr, nullptr, nullptr, is_cipher) != 1) {
26+
if (EVP_CipherInit_ex(ctx.get(), cipher, nullptr, nullptr, nullptr, is_cipher) != 1) {
3027
unsigned long err = ERR_get_error();
3128
char err_buf[256];
3229
ERR_error_string_n(err, err_buf, sizeof(err_buf));
33-
EVP_CIPHER_CTX_free(ctx);
34-
ctx = nullptr;
30+
ctx.reset();
3531
throw std::runtime_error("ChaCha20Poly1305Cipher: Failed initial CipherInit setup: " + std::string(err_buf));
3632
}
3733

@@ -52,12 +48,11 @@ void ChaCha20Poly1305Cipher::init(const std::shared_ptr<ArrayBuffer> cipher_key,
5248
const unsigned char* key_ptr = reinterpret_cast<const unsigned char*>(native_key->data());
5349
const unsigned char* iv_ptr = reinterpret_cast<const unsigned char*>(native_iv->data());
5450

55-
if (EVP_CipherInit_ex(ctx, nullptr, nullptr, key_ptr, iv_ptr, is_cipher) != 1) {
51+
if (EVP_CipherInit_ex(ctx.get(), nullptr, nullptr, key_ptr, iv_ptr, is_cipher) != 1) {
5652
unsigned long err = ERR_get_error();
5753
char err_buf[256];
5854
ERR_error_string_n(err, err_buf, sizeof(err_buf));
59-
EVP_CIPHER_CTX_free(ctx);
60-
ctx = nullptr;
55+
ctx.reset();
6156
throw std::runtime_error("ChaCha20Poly1305Cipher: Failed to set key/IV: " + std::string(err_buf));
6257
}
6358
is_finalized = false;
@@ -77,7 +72,7 @@ std::shared_ptr<ArrayBuffer> ChaCha20Poly1305Cipher::update(const std::shared_pt
7772
uint8_t* out = new uint8_t[out_len];
7873

7974
// Perform the cipher update operation
80-
if (EVP_CipherUpdate(ctx, out, &out_len, native_data->data(), in_len) != 1) {
75+
if (EVP_CipherUpdate(ctx.get(), out, &out_len, native_data->data(), in_len) != 1) {
8176
delete[] out;
8277
unsigned long err = ERR_get_error();
8378
char err_buf[256];
@@ -97,7 +92,7 @@ std::shared_ptr<ArrayBuffer> ChaCha20Poly1305Cipher::final() {
9792
int out_len = 0;
9893
unsigned char* out = new unsigned char[0];
9994

100-
if (EVP_CipherFinal_ex(ctx, out, &out_len) != 1) {
95+
if (EVP_CipherFinal_ex(ctx.get(), out, &out_len) != 1) {
10196
delete[] out;
10297
unsigned long err = ERR_get_error();
10398
char err_buf[256];
@@ -116,7 +111,7 @@ bool ChaCha20Poly1305Cipher::setAAD(const std::shared_ptr<ArrayBuffer>& data, st
116111

117112
// Set AAD data
118113
int out_len = 0;
119-
if (EVP_CipherUpdate(ctx, nullptr, &out_len, native_aad->data(), aad_len) != 1) {
114+
if (EVP_CipherUpdate(ctx.get(), nullptr, &out_len, native_aad->data(), aad_len) != 1) {
120115
unsigned long err = ERR_get_error();
121116
char err_buf[256];
122117
ERR_error_string_n(err, err_buf, sizeof(err_buf));
@@ -136,7 +131,7 @@ std::shared_ptr<ArrayBuffer> ChaCha20Poly1305Cipher::getAuthTag() {
136131

137132
// Get the authentication tag
138133
auto tag_buf = std::make_unique<uint8_t[]>(kTagSize);
139-
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, kTagSize, tag_buf.get()) != 1) {
134+
if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_AEAD_GET_TAG, kTagSize, tag_buf.get()) != 1) {
140135
unsigned long err = ERR_get_error();
141136
char err_buf[256];
142137
ERR_error_string_n(err, err_buf, sizeof(err_buf));
@@ -158,7 +153,7 @@ bool ChaCha20Poly1305Cipher::setAuthTag(const std::shared_ptr<ArrayBuffer>& tag)
158153
throw std::runtime_error("ChaCha20-Poly1305 tag must be 16 bytes");
159154
}
160155

161-
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, kTagSize, native_tag->data()) != 1) {
156+
if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_AEAD_SET_TAG, kTagSize, native_tag->data()) != 1) {
162157
unsigned long err = ERR_get_error();
163158
char err_buf[256];
164159
ERR_error_string_n(err, err_buf, sizeof(err_buf));

packages/react-native-quick-crypto/cpp/cipher/ChaCha20Poly1305Cipher.hpp

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@ namespace margelo::nitro::crypto {
77
class ChaCha20Poly1305Cipher : public HybridCipher {
88
public:
99
ChaCha20Poly1305Cipher() : HybridObject(TAG) {}
10-
~ChaCha20Poly1305Cipher() {
11-
// Let parent destructor free the context
12-
ctx = nullptr;
13-
}
10+
// Destructor defaulted: HybridCipher's unique_ptr ctx frees itself.
11+
~ChaCha20Poly1305Cipher() override = default;
1412

1513
void init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::shared_ptr<ArrayBuffer> iv) override;
1614
std::shared_ptr<ArrayBuffer> update(const std::shared_ptr<ArrayBuffer>& data) override;

packages/react-native-quick-crypto/cpp/cipher/GCMCipher.cpp

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,8 @@
77
namespace margelo::nitro::crypto {
88

99
void GCMCipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::shared_ptr<ArrayBuffer> iv) {
10-
// Clean up any existing context
11-
if (ctx) {
12-
EVP_CIPHER_CTX_free(ctx);
13-
ctx = nullptr;
14-
}
10+
// Resetting the unique_ptr frees any previous context.
11+
ctx.reset();
1512

1613
// 1. Get cipher implementation by name
1714
const EVP_CIPHER* cipher = EVP_get_cipherbyname(cipher_type.c_str());
@@ -20,18 +17,17 @@ void GCMCipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::s
2017
}
2118

2219
// 2. Create a new context
23-
ctx = EVP_CIPHER_CTX_new();
20+
ctx.reset(EVP_CIPHER_CTX_new());
2421
if (!ctx) {
2522
throw std::runtime_error("Failed to create cipher context");
2623
}
2724

2825
// 3. Initialize with cipher type only (no key/IV yet)
29-
if (EVP_CipherInit_ex(ctx, cipher, nullptr, nullptr, nullptr, is_cipher) != 1) {
26+
if (EVP_CipherInit_ex(ctx.get(), cipher, nullptr, nullptr, nullptr, is_cipher) != 1) {
3027
unsigned long err = ERR_get_error();
3128
char err_buf[256];
3229
ERR_error_string_n(err, err_buf, sizeof(err_buf));
33-
EVP_CIPHER_CTX_free(ctx);
34-
ctx = nullptr;
30+
ctx.reset();
3531
throw std::runtime_error("GCMCipher: Failed initial CipherInit setup: " + std::string(err_buf));
3632
}
3733

@@ -40,12 +36,11 @@ void GCMCipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::s
4036
size_t iv_len = native_iv->size();
4137

4238
if (iv_len != 12) { // Only set if not the default length
43-
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, static_cast<int>(iv_len), nullptr) != 1) {
39+
if (EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, static_cast<int>(iv_len), nullptr) != 1) {
4440
unsigned long err = ERR_get_error();
4541
char err_buf[256];
4642
ERR_error_string_n(err, err_buf, sizeof(err_buf));
47-
EVP_CIPHER_CTX_free(ctx);
48-
ctx = nullptr;
43+
ctx.reset();
4944
throw std::runtime_error("GCMCipher: Failed to set IV length: " + std::string(err_buf));
5045
}
5146
}
@@ -55,12 +50,11 @@ void GCMCipher::init(const std::shared_ptr<ArrayBuffer> cipher_key, const std::s
5550
const unsigned char* key_ptr = reinterpret_cast<const unsigned char*>(native_key->data());
5651
const unsigned char* iv_ptr = reinterpret_cast<const unsigned char*>(native_iv->data());
5752

58-
if (EVP_CipherInit_ex(ctx, nullptr, nullptr, key_ptr, iv_ptr, is_cipher) != 1) {
53+
if (EVP_CipherInit_ex(ctx.get(), nullptr, nullptr, key_ptr, iv_ptr, is_cipher) != 1) {
5954
unsigned long err = ERR_get_error();
6055
char err_buf[256];
6156
ERR_error_string_n(err, err_buf, sizeof(err_buf));
62-
EVP_CIPHER_CTX_free(ctx);
63-
ctx = nullptr;
57+
ctx.reset();
6458
throw std::runtime_error("GCMCipher: Failed to set key/IV: " + std::string(err_buf));
6559
}
6660
}

0 commit comments

Comments
 (0)