Skip to content

Commit e9e2c31

Browse files
orgadsclaude
andcommitted
src: don't fail startup on unreadable OpenSSL config
OpenSSL is initialized with CONF_MFLAGS_IGNORE_MISSING_FILE so that a missing configuration file does not prevent Node.js from starting, but that flag does not cover a file that exists and cannot be opened. Running in a container where /etc/ssl is not accessible to the current user therefore aborts startup with: OpenSSL configuration error: ...:BIO_new_file:Permission denied:...fopen(/etc/ssl/openssl.cnf, rb) Ignore the error when the unreadable file is the default configuration file, that is, one that was not requested via OPENSSL_CONF or --openssl-config. Errors from an explicitly requested file, and from the files it includes, stay fatal. Fixes: #62230 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Orgad Shaneh <orgad.shaneh@audiocodes.com>
1 parent bb76938 commit e9e2c31

2 files changed

Lines changed: 56 additions & 0 deletions

File tree

‎src/node.cc‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
#include "node_version.h"
4949

5050
#if HAVE_OPENSSL
51+
#include <openssl/conf.h>
5152
#include "ncrypto.h"
5253
#include "node_crypto.h"
5354
#if OPENSSL_VERSION_MAJOR >= 3 && !defined(CONF_MFLAGS_IGNORE_MISSING_FILE)
@@ -135,6 +136,7 @@
135136
#include <cstdlib>
136137
#include <cstring>
137138

139+
#include <fstream>
138140
#include <string>
139141
#include <tuple>
140142
#include <vector>
@@ -1099,6 +1101,17 @@ bool CanEnableWebAssemblyTrapHandler() {
10991101
}
11001102
#endif // NODE_USE_V8_WASM_TRAP_HANDLER
11011103

1104+
#if HAVE_OPENSSL && !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_MAJOR >= 3
1105+
// Whether the configuration file that OpenSSL uses when none was requested
1106+
// explicitly can be opened for reading.
1107+
static bool DefaultOpenSSLConfIsReadable() {
1108+
char* path = CONF_get1_default_config_file();
1109+
if (path == nullptr) return false;
1110+
auto free_path = OnScopeLeave([&]() { OPENSSL_free(path); });
1111+
return std::ifstream(path).is_open();
1112+
}
1113+
#endif
1114+
11021115
static std::shared_ptr<InitializationResultImpl>
11031116
InitializeOncePerProcessInternal(const std::vector<std::string>& args,
11041117
ProcessInitializationFlags::Flags flags =
@@ -1229,6 +1242,16 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args,
12291242
OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CONFIG, settings);
12301243
OPENSSL_INIT_free(settings);
12311244

1245+
// CONF_MFLAGS_IGNORE_MISSING_FILE only covers a missing file, not a default
1246+
// configuration file that exists but cannot be opened, e.g. when /etc/ssl
1247+
// is not readable by the current user. Loading it was not something the
1248+
// user asked for, so ignore that failure as well instead of refusing to
1249+
// start. Refs: https://github.com/nodejs/node/issues/62230
1250+
if (ERR_peek_error() != 0 && conf_file == nullptr &&
1251+
!DefaultOpenSSLConfIsReadable()) {
1252+
ERR_clear_error();
1253+
}
1254+
12321255
if (ERR_peek_error() != 0) {
12331256
// XXX: ERR_GET_REASON does not return something that is
12341257
// useful as an exit code at all.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use strict';
2+
3+
// An unreadable default OpenSSL configuration file must not abort startup.
4+
// Refs: https://github.com/nodejs/node/issues/62230
5+
6+
const common = require('../common');
7+
const assert = require('node:assert');
8+
const { spawnSync } = require('node:child_process');
9+
10+
if (!common.hasCrypto)
11+
common.skip('missing crypto');
12+
if (!common.isLinux)
13+
common.skip('linux only');
14+
if (process.config.variables.node_shared_openssl)
15+
common.skip('shared openssl may read a different configuration file');
16+
17+
// Replace /etc/ssl with an empty tmpfs in a private mount namespace, where
18+
// openssl.cnf is a symlink loop: opening it then fails with ELOOP instead of
19+
// ENOENT, which OpenSSL ignores on its own. The namespace goes away with the
20+
// process, so the host /etc/ssl is left alone.
21+
const setup = 'mount -t tmpfs tmpfs /etc/ssl && ln -s openssl.cnf /etc/ssl/openssl.cnf';
22+
23+
if (spawnSync('unshare', ['-Urm', 'sh', '-c', setup]).status !== 0)
24+
common.skip('cannot set up an unprivileged user and mount namespace');
25+
26+
const child = spawnSync(
27+
'unshare',
28+
['-Urm', 'sh', '-c', `${setup} && exec "$0" -p 42`, process.execPath],
29+
{ encoding: 'utf8' });
30+
31+
assert.strictEqual(child.stderr, '');
32+
assert.strictEqual(child.status, 0);
33+
assert.strictEqual(child.stdout.trim(), '42');

0 commit comments

Comments
 (0)