Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

src: refactor and harden ProcessEmitWarning() #17420

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ class ModuleWrap;
V(dns_txt_string, "TXT") \
V(domain_string, "domain") \
V(emit_string, "emit") \
V(emit_warning_string, "emitWarning") \
V(exchange_string, "exchange") \
V(enumerable_string, "enumerable") \
V(idle_string, "idle") \
Expand Down
90 changes: 73 additions & 17 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,15 @@ using v8::HandleScope;
using v8::HeapStatistics;
using v8::Integer;
using v8::Isolate;
using v8::Just;
using v8::Local;
using v8::Locker;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Message;
using v8::Name;
using v8::NamedPropertyHandlerConfiguration;
using v8::Nothing;
using v8::Null;
using v8::Number;
using v8::Object;
Expand Down Expand Up @@ -2277,8 +2280,11 @@ static void DLOpen(const FunctionCallbackInfo<Value>& args) {
}
if (mp->nm_version == -1) {
if (env->EmitNapiWarning()) {
ProcessEmitWarning(env, "N-API is an experimental feature and could "
"change at any time.");
if (ProcessEmitWarning(env, "N-API is an experimental feature and could "
"change at any time.").IsNothing()) {
dlib.Close();
return;
}
}
} else if (mp->nm_version != NODE_MODULE_VERSION) {
char errmsg[1024];
Expand Down Expand Up @@ -2425,33 +2431,83 @@ static void OnMessage(Local<Message> message, Local<Value> error) {
FatalException(Isolate::GetCurrent(), error, message);
}

static Maybe<bool> ProcessEmitWarningGeneric(Environment* env,
const char* warning,
const char* type = nullptr,
const char* code = nullptr) {
HandleScope handle_scope(env->isolate());
Context::Scope context_scope(env->context());

Local<Object> process = env->process_object();
Local<Value> emit_warning;
if (!process->Get(env->context(),
env->emit_warning_string()).ToLocal(&emit_warning)) {
return Nothing<bool>();
}

if (!emit_warning->IsFunction()) return Just(false);

int argc = 0;
Local<Value> args[3]; // warning, type, code

// The caller has to be able to handle a failure anyway, so we might as well
// do proper error checking for string creation.
if (!String::NewFromUtf8(env->isolate(),
warning,
v8::NewStringType::kNormal).ToLocal(&args[argc++])) {
return Nothing<bool>();
}
if (type != nullptr) {
if (!String::NewFromOneByte(env->isolate(),
reinterpret_cast<const uint8_t*>(type),
v8::NewStringType::kNormal)
.ToLocal(&args[argc++])) {
return Nothing<bool>();
}
if (code != nullptr &&
!String::NewFromOneByte(env->isolate(),
reinterpret_cast<const uint8_t*>(code),
v8::NewStringType::kNormal)
.ToLocal(&args[argc++])) {
return Nothing<bool>();
}
}

// MakeCallback() unneeded because emitWarning is internal code, it calls
// process.emit('warning', ...), but does so on the nextTick.
if (emit_warning.As<Function>()->Call(env->context(),
Copy link
Member

@AndreasMadsen AndreasMadsen Dec 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If process.emitWarning is supposed to be overwritable, as your test says. Then we should use MakeCallback, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mh … I am not sure. There is usually always a JS call stack below, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, that won't be true for the domain MakeCallback deprecation. Anything could call MakeCallback in that case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. True. But that would put us in a pretty difficult position if process.domain happens to be set in that case (by accident), since process would be the resource object?

So, I am okay with changing this but I’m not sure where we’d get the async context to use for MakeCallback?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I am okay with changing this but I’m not sure where we’d get the async context to use for MakeCallback?

Yes. This is a general problem. We have process.on('beforeExit'), process.on('exit') without context as well. In this case I think there are two options:

  • Have a general async_context for the process, and use that.
  • Create a new async_context with async_context_.async_id as the triggerAsyncId.

The latter option is better for this case but I don't see it working in general for all ProcessEmitWarningGeneric use cases.

process,
argc,
args).IsEmpty()) {
return Nothing<bool>();
}
return Just(true);
}


// Call process.emitWarning(str), fmt is a snprintf() format string
void ProcessEmitWarning(Environment* env, const char* fmt, ...) {
Maybe<bool> ProcessEmitWarning(Environment* env, const char* fmt, ...) {
char warning[1024];
va_list ap;

va_start(ap, fmt);
vsnprintf(warning, sizeof(warning), fmt, ap);
va_end(ap);

HandleScope handle_scope(env->isolate());
Context::Scope context_scope(env->context());

Local<Object> process = env->process_object();
MaybeLocal<Value> emit_warning = process->Get(env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "emitWarning"));
Local<Value> arg = node::OneByteString(env->isolate(), warning);

Local<Value> f;
return ProcessEmitWarningGeneric(env, warning);
}

if (!emit_warning.ToLocal(&f)) return;
if (!f->IsFunction()) return;

// MakeCallback() unneeded, because emitWarning is internal code, it calls
// process.emit('warning', ..), but does so on the nextTick.
f.As<v8::Function>()->Call(process, 1, &arg);
Maybe<bool> ProcessEmitDeprecationWarning(Environment* env,
const char* warning,
const char* deprecation_code) {
return ProcessEmitWarningGeneric(env,
warning,
"DeprecationWarning",
deprecation_code);
}


static bool PullFromCache(Environment* env,
const FunctionCallbackInfo<Value>& args,
Local<String> module,
Expand Down
11 changes: 9 additions & 2 deletions src/node_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1062,8 +1062,12 @@ void SecureContext::AddRootCerts(const FunctionCallbackInfo<Value>& args) {
root_cert_store,
extra_root_certs_file.c_str());
if (err) {
// We do not call back into JS after this line anyway, so ignoring
// the return value of ProcessEmitWarning does not affect how a
// possible exception would be propagated.
ProcessEmitWarning(sc->env(),
"Ignoring extra certs from `%s`, load failed: %s\n",
"Ignoring extra certs from `%s`, "
"load failed: %s\n",
extra_root_certs_file.c_str(),
ERR_error_string(err, nullptr));
}
Expand Down Expand Up @@ -3618,7 +3622,10 @@ void CipherBase::Init(const char* cipher_type,
int mode = EVP_CIPHER_CTX_mode(ctx_);
if (encrypt && (mode == EVP_CIPH_CTR_MODE || mode == EVP_CIPH_GCM_MODE ||
mode == EVP_CIPH_CCM_MODE)) {
ProcessEmitWarning(env(), "Use Cipheriv for counter mode of %s",
// Ignore the return value (i.e. possible exception) because we are
// not calling back into JS anyway.
ProcessEmitWarning(env(),
"Use Cipheriv for counter mode of %s",
cipher_type);
}

Expand Down
5 changes: 4 additions & 1 deletion src/node_internals.h
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,10 @@ class FatalTryCatch : public v8::TryCatch {
Environment* env_;
};

void ProcessEmitWarning(Environment* env, const char* fmt, ...);
v8::Maybe<bool> ProcessEmitWarning(Environment* env, const char* fmt, ...);
v8::Maybe<bool> ProcessEmitDeprecationWarning(Environment* env,
const char* warning,
const char* deprecation_code);

void FillStatsArray(double* fields, const uv_stat_t* s);

Expand Down
46 changes: 46 additions & 0 deletions test/parallel/test-process-emit-warning-from-native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use strict';
const common = require('../common');
const assert = require('assert');

if (!common.hasCrypto)
common.skip('missing crypto');
if (common.hasFipsCrypto)
common.skip('crypto.createCipher() is not supported in FIPS mode');

const crypto = require('crypto');
const key = '0123456789';

{
common.expectWarning('Warning',
'Use Cipheriv for counter mode of aes-256-gcm');

// Emits regular warning expected by expectWarning()
crypto.createCipher('aes-256-gcm', key);
}

const realEmitWarning = process.emitWarning;

{
// It's a good idea to make this overridable from userland.
process.emitWarning = () => { throw new Error('foo'); };
assert.throws(() => {
crypto.createCipher('aes-256-gcm', key);
}, /^Error: foo$/);
}

{
Object.defineProperty(process, 'emitWarning', {
get() { throw new Error('bar'); },
configurable: true
});
assert.throws(() => {
crypto.createCipher('aes-256-gcm', key);
}, /^Error: bar$/);
}

// Reset back to default after the test.
Object.defineProperty(process, 'emitWarning', {
value: realEmitWarning,
configurable: true,
writable: true
});
10 changes: 7 additions & 3 deletions test/parallel/test-tls-env-bad-extra-ca.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ if (process.env.CHILD) {

const env = Object.assign({}, process.env, {
CHILD: 'yes',
NODE_EXTRA_CA_CERTS: `${fixtures.fixturesDir}/no-such-file-exists`,
NODE_EXTRA_CA_CERTS: `${fixtures.fixturesDir}/no-such-file-exists-🐢`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha :-)

});

const opts = {
Expand All @@ -32,8 +32,12 @@ fork(__filename, opts)
assert.strictEqual(status, 0, 'client did not succeed in connecting');
}))
.on('close', common.mustCall(function() {
const re = /Warning: Ignoring extra certs from.*no-such-file-exists.* load failed:.*No such file or directory/;
assert(re.test(stderr), stderr);
// TODO(addaleax): Make `SafeGetenv` work like `process.env`
// encoding-wise
if (!common.isWindows) {
const re = /Warning: Ignoring extra certs from.*no-such-file-exists-🐢.* load failed:.*No such file or directory/;
assert(re.test(stderr), stderr);
}
}))
.stderr.setEncoding('utf8').on('data', function(str) {
stderr += str;
Expand Down