diff --git a/src/node.cc b/src/node.cc index b368c5873434..38dce1725d1f 100644 --- a/src/node.cc +++ b/src/node.cc @@ -48,6 +48,7 @@ #include "node_version.h" #if HAVE_OPENSSL +#include #include "ncrypto.h" #include "node_crypto.h" #if OPENSSL_VERSION_MAJOR >= 3 && !defined(CONF_MFLAGS_IGNORE_MISSING_FILE) @@ -135,6 +136,7 @@ #include #include +#include #include #include #include @@ -1099,6 +1101,17 @@ bool CanEnableWebAssemblyTrapHandler() { } #endif // NODE_USE_V8_WASM_TRAP_HANDLER +#if HAVE_OPENSSL && !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_MAJOR >= 3 +// Whether the configuration file that OpenSSL uses when none was requested +// explicitly can be opened for reading. +static bool DefaultOpenSSLConfIsReadable() { + char* path = CONF_get1_default_config_file(); + if (path == nullptr) return false; + auto free_path = OnScopeLeave([&]() { OPENSSL_free(path); }); + return std::ifstream(path).is_open(); +} +#endif + static std::shared_ptr InitializeOncePerProcessInternal(const std::vector& args, ProcessInitializationFlags::Flags flags = @@ -1229,6 +1242,16 @@ InitializeOncePerProcessInternal(const std::vector& args, OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CONFIG, settings); OPENSSL_INIT_free(settings); + // CONF_MFLAGS_IGNORE_MISSING_FILE only covers a missing file, not a default + // configuration file that exists but cannot be opened, e.g. when /etc/ssl + // is not readable by the current user. Loading it was not something the + // user asked for, so ignore that failure as well instead of refusing to + // start. Refs: https://github.com/nodejs/node/issues/62230 + if (ERR_peek_error() != 0 && conf_file == nullptr && + !DefaultOpenSSLConfIsReadable()) { + ERR_clear_error(); + } + if (ERR_peek_error() != 0) { // XXX: ERR_GET_REASON does not return something that is // useful as an exit code at all. diff --git a/test/parallel/test-openssl-unreadable-config.js b/test/parallel/test-openssl-unreadable-config.js new file mode 100644 index 000000000000..f77f337d053f --- /dev/null +++ b/test/parallel/test-openssl-unreadable-config.js @@ -0,0 +1,33 @@ +'use strict'; + +// An unreadable default OpenSSL configuration file must not abort startup. +// Refs: https://github.com/nodejs/node/issues/62230 + +const common = require('../common'); +const assert = require('node:assert'); +const { spawnSync } = require('node:child_process'); + +if (!common.hasCrypto) + common.skip('missing crypto'); +if (!common.isLinux) + common.skip('linux only'); +if (process.config.variables.node_shared_openssl) + common.skip('shared openssl may read a different configuration file'); + +// Replace /etc/ssl with an empty tmpfs in a private mount namespace, where +// openssl.cnf is a symlink loop: opening it then fails with ELOOP instead of +// ENOENT, which OpenSSL ignores on its own. The namespace goes away with the +// process, so the host /etc/ssl is left alone. +const setup = 'mount -t tmpfs tmpfs /etc/ssl && ln -s openssl.cnf /etc/ssl/openssl.cnf'; + +if (spawnSync('unshare', ['-Urm', 'sh', '-c', setup]).status !== 0) + common.skip('cannot set up an unprivileged user and mount namespace'); + +const child = spawnSync( + 'unshare', + ['-Urm', 'sh', '-c', `${setup} && exec "$0" -p 42`, process.execPath], + { encoding: 'utf8' }); + +assert.strictEqual(child.stderr, ''); +assert.strictEqual(child.status, 0); +assert.strictEqual(child.stdout.trim(), '42');