Skip to content

Commit

Permalink
sea: add option to disable the experimental SEA warning
Browse files Browse the repository at this point in the history
Refs: nodejs/single-executable#60
Signed-off-by: Darshan Sen <raisinten@gmail.com>
PR-URL: #47588
Fixes: #47741
Reviewed-By: Michael Dawson <midawson@redhat.com>
Reviewed-By: Tierney Cyren <hello@bnb.im>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
  • Loading branch information
RaisinTen authored and targos committed May 12, 2023
1 parent 1a7fc18 commit c4596b9
Show file tree
Hide file tree
Showing 14 changed files with 444 additions and 150 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
/src/node_sea* @nodejs/single-executable
/test/fixtures/postject-copy @nodejs/single-executable
/test/parallel/test-single-executable-* @nodejs/single-executable
/test/sequential/test-single-executable-* @nodejs/single-executable
/tools/dep_updaters/update-postject.sh @nodejs/single-executable

# Permission Model
Expand Down
3 changes: 2 additions & 1 deletion doc/api/single-executable-applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ The configuration currently reads the following top-level fields:
```json
{
"main": "/path/to/bundled/script.js",
"output": "/path/to/write/the/generated/blob.blob"
"output": "/path/to/write/the/generated/blob.blob",
"disableExperimentalSEAWarning": true // Default: false
}
```

Expand Down
4 changes: 2 additions & 2 deletions lib/internal/main/embedding.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ const {
prepareMainThreadExecution,
markBootstrapComplete,
} = require('internal/process/pre_execution');
const { isSea } = internalBinding('sea');
const { isExperimentalSeaWarningNeeded } = internalBinding('sea');
const { emitExperimentalWarning } = require('internal/util');
const { embedderRequire, embedderRunCjs } = require('internal/util/embedding');
const { getEmbedderEntryFunction } = internalBinding('mksnapshot');

prepareMainThreadExecution(false, true);
markBootstrapComplete();

if (isSea()) {
if (isExperimentalSeaWarningNeeded()) {
emitExperimentalWarning('Single executable application');
}

Expand Down
38 changes: 33 additions & 5 deletions src/json_parser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,51 @@ bool JSONParser::Parse(const std::string& content) {
return true;
}

std::optional<std::string> JSONParser::GetTopLevelField(
const std::string& field) {
std::optional<std::string> JSONParser::GetTopLevelStringField(
std::string_view field) {
Isolate* isolate = isolate_.get();
Local<Context> context = context_.Get(isolate);
Local<Object> content_object = content_.Get(isolate);
Local<Value> value;
// It's not a real script, so don't print the source line.
errors::PrinterTryCatch bootstrapCatch(
isolate, errors::PrinterTryCatch::kDontPrintSourceLine);
if (!content_object
->Get(context, OneByteString(isolate, field.c_str(), field.length()))
.ToLocal(&value) ||
Local<Value> field_local;
if (!ToV8Value(context, field, isolate).ToLocal(&field_local)) {
return {};
}
if (!content_object->Get(context, field_local).ToLocal(&value) ||
!value->IsString()) {
return {};
}
Utf8Value utf8_value(isolate, value);
return utf8_value.ToString();
}

std::optional<bool> JSONParser::GetTopLevelBoolField(std::string_view field) {
Isolate* isolate = isolate_.get();
Local<Context> context = context_.Get(isolate);
Local<Object> content_object = content_.Get(isolate);
Local<Value> value;
bool has_field;
// It's not a real script, so don't print the source line.
errors::PrinterTryCatch bootstrapCatch(
isolate, errors::PrinterTryCatch::kDontPrintSourceLine);
Local<Value> field_local;
if (!ToV8Value(context, field, isolate).ToLocal(&field_local)) {
return {};
}
if (!content_object->Has(context, field_local).To(&has_field)) {
return {};
}
if (!has_field) {
return false;
}
if (!content_object->Get(context, field_local).ToLocal(&value) ||
!value->IsBoolean()) {
return {};
}
return value->BooleanValue(isolate);
}

} // namespace node
3 changes: 2 additions & 1 deletion src/json_parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ class JSONParser {
JSONParser();
~JSONParser() {}
bool Parse(const std::string& content);
std::optional<std::string> GetTopLevelField(const std::string& field);
std::optional<std::string> GetTopLevelStringField(std::string_view field);
std::optional<bool> GetTopLevelBoolField(std::string_view field);

private:
// We might want a lighter-weight JSON parser for this use case. But for now
Expand Down
91 changes: 79 additions & 12 deletions src/node_sea.cc
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,41 @@ using v8::Value;
namespace node {
namespace sea {

namespace {
// A special number that will appear at the beginning of the single executable
// preparation blobs ready to be injected into the binary. We use this to check
// that the data given to us are intended for building single executable
// applications.
static const uint32_t kMagic = 0x143da20;
const uint32_t kMagic = 0x143da20;

std::string_view FindSingleExecutableCode() {
enum class SeaFlags : uint32_t {
kDefault = 0,
kDisableExperimentalSeaWarning = 1 << 0,
};

SeaFlags operator|(SeaFlags x, SeaFlags y) {
return static_cast<SeaFlags>(static_cast<uint32_t>(x) |
static_cast<uint32_t>(y));
}

SeaFlags operator&(SeaFlags x, SeaFlags y) {
return static_cast<SeaFlags>(static_cast<uint32_t>(x) &
static_cast<uint32_t>(y));
}

SeaFlags operator|=(/* NOLINT (runtime/references) */ SeaFlags& x, SeaFlags y) {
return x = x | y;
}

struct SeaResource {
SeaFlags flags = SeaFlags::kDefault;
std::string_view code;
static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags);
};

SeaResource FindSingleExecutableResource() {
CHECK(IsSingleExecutable());
static const std::string_view sea_code = []() -> std::string_view {
static const SeaResource sea_resource = []() -> SeaResource {
size_t size;
#ifdef __APPLE__
postject_options options;
Expand All @@ -55,18 +81,40 @@ std::string_view FindSingleExecutableCode() {
#endif
uint32_t first_word = reinterpret_cast<const uint32_t*>(code)[0];
CHECK_EQ(first_word, kMagic);
SeaFlags flags{
reinterpret_cast<const SeaFlags*>(code + sizeof(first_word))[0]};
// TODO(joyeecheung): do more checks here e.g. matching the versions.
return {code + sizeof(first_word), size - sizeof(first_word)};
return {
flags,
{
code + SeaResource::kHeaderSize,
size - SeaResource::kHeaderSize,
},
};
}();
return sea_code;
return sea_resource;
}

} // namespace

std::string_view FindSingleExecutableCode() {
SeaResource sea_resource = FindSingleExecutableResource();
return sea_resource.code;
}

bool IsSingleExecutable() {
return postject_has_resource();
}

void IsSingleExecutable(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(IsSingleExecutable());
void IsExperimentalSeaWarningNeeded(const FunctionCallbackInfo<Value>& args) {
if (!IsSingleExecutable()) {
args.GetReturnValue().Set(false);
return;
}

SeaResource sea_resource = FindSingleExecutableResource();
args.GetReturnValue().Set(!static_cast<bool>(
sea_resource.flags & SeaFlags::kDisableExperimentalSeaWarning));
}

std::tuple<int, char**> FixupArgsForSEA(int argc, char** argv) {
Expand All @@ -90,6 +138,7 @@ namespace {
struct SeaConfig {
std::string main_path;
std::string output_path;
SeaFlags flags = SeaFlags::kDefault;
};

std::optional<SeaConfig> ParseSingleExecutableConfig(
Expand All @@ -112,7 +161,8 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
return std::nullopt;
}

result.main_path = parser.GetTopLevelField("main").value_or(std::string());
result.main_path =
parser.GetTopLevelStringField("main").value_or(std::string());
if (result.main_path.empty()) {
FPrintF(stderr,
"\"main\" field of %s is not a non-empty string\n",
Expand All @@ -121,14 +171,26 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
}

result.output_path =
parser.GetTopLevelField("output").value_or(std::string());
parser.GetTopLevelStringField("output").value_or(std::string());
if (result.output_path.empty()) {
FPrintF(stderr,
"\"output\" field of %s is not a non-empty string\n",
config_path);
return std::nullopt;
}

std::optional<bool> disable_experimental_sea_warning =
parser.GetTopLevelBoolField("disableExperimentalSEAWarning");
if (!disable_experimental_sea_warning.has_value()) {
FPrintF(stderr,
"\"disableExperimentalSEAWarning\" field of %s is not a Boolean\n",
config_path);
return std::nullopt;
}
if (disable_experimental_sea_warning.value()) {
result.flags |= SeaFlags::kDisableExperimentalSeaWarning;
}

return result;
}

Expand All @@ -144,9 +206,11 @@ bool GenerateSingleExecutableBlob(const SeaConfig& config) {

std::vector<char> sink;
// TODO(joyeecheung): reuse the SnapshotSerializerDeserializer for this.
sink.reserve(sizeof(kMagic) + main_script.size());
sink.reserve(SeaResource::kHeaderSize + main_script.size());
const char* pos = reinterpret_cast<const char*>(&kMagic);
sink.insert(sink.end(), pos, pos + sizeof(kMagic));
pos = reinterpret_cast<const char*>(&(config.flags));
sink.insert(sink.end(), pos, pos + sizeof(SeaFlags));
sink.insert(
sink.end(), main_script.data(), main_script.data() + main_script.size());

Expand Down Expand Up @@ -181,11 +245,14 @@ void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
SetMethod(context, target, "isSea", IsSingleExecutable);
SetMethod(context,
target,
"isExperimentalSeaWarningNeeded",
IsExperimentalSeaWarningNeeded);
}

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(IsSingleExecutable);
registry->Register(IsExperimentalSeaWarningNeeded);
}

} // namespace sea
Expand Down
16 changes: 16 additions & 0 deletions test/common/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,22 @@ Validates the schema of a diagnostic report file whose path is specified in
Validates the schema of a diagnostic report whose content is specified in
`report`. If the report fails validation, an exception is thrown.

## SEA Module

The `sea` module provides helper functions for testing Single Executable
Application functionality.

### `skipIfSingleExecutableIsNotSupported()`

Skip the rest of the tests if single executable applications are not supported
in the current configuration.

### `injectAndCodeSign(targetExecutable, resource)`

Uses Postect to inject the contents of the file at the path `resource` into
the target executable file at the path `targetExecutable` and ultimately code
sign the final binary.

## tick Module

The `tick` module provides a helper function that can be used to call a callback
Expand Down
92 changes: 92 additions & 0 deletions test/common/sea.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
'use strict';

const common = require('../common');
const fixtures = require('../common/fixtures');

const { readFileSync } = require('fs');
const { execFileSync } = require('child_process');

function skipIfSingleExecutableIsNotSupported() {
if (!process.config.variables.single_executable_application)
common.skip('Single Executable Application support has been disabled.');

if (!['darwin', 'win32', 'linux'].includes(process.platform))
common.skip(`Unsupported platform ${process.platform}.`);

if (process.platform === 'linux' && process.config.variables.is_debug === 1)
common.skip('Running the resultant binary fails with `Couldn\'t read target executable"`.');

if (process.config.variables.node_shared)
common.skip('Running the resultant binary fails with ' +
'`/home/iojs/node-tmp/.tmp.2366/sea: error while loading shared libraries: ' +
'libnode.so.112: cannot open shared object file: No such file or directory`.');

if (process.config.variables.icu_gyp_path === 'tools/icu/icu-system.gyp')
common.skip('Running the resultant binary fails with ' +
'`/home/iojs/node-tmp/.tmp.2379/sea: error while loading shared libraries: ' +
'libicui18n.so.71: cannot open shared object file: No such file or directory`.');

if (!process.config.variables.node_use_openssl || process.config.variables.node_shared_openssl)
common.skip('Running the resultant binary fails with `Node.js is not compiled with OpenSSL crypto support`.');

if (process.config.variables.want_separate_host_toolset !== 0)
common.skip('Running the resultant binary fails with `Segmentation fault (core dumped)`.');

if (process.platform === 'linux') {
const osReleaseText = readFileSync('/etc/os-release', { encoding: 'utf-8' });
const isAlpine = /^NAME="Alpine Linux"/m.test(osReleaseText);
if (isAlpine) common.skip('Alpine Linux is not supported.');

if (process.arch === 's390x') {
common.skip('On s390x, postject fails with `memory access out of bounds`.');
}

if (process.arch === 'ppc64') {
common.skip('On ppc64, this test times out.');
}
}
}

function injectAndCodeSign(targetExecutable, resource) {
const postjectFile = fixtures.path('postject-copy', 'node_modules', 'postject', 'dist', 'cli.js');
execFileSync(process.execPath, [
postjectFile,
targetExecutable,
'NODE_SEA_BLOB',
resource,
'--sentinel-fuse', 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2',
...process.platform === 'darwin' ? [ '--macho-segment-name', 'NODE_SEA' ] : [],
]);

if (process.platform === 'darwin') {
execFileSync('codesign', [ '--sign', '-', targetExecutable ]);
execFileSync('codesign', [ '--verify', targetExecutable ]);
} else if (process.platform === 'win32') {
let signtoolFound = false;
try {
execFileSync('where', [ 'signtool' ]);
signtoolFound = true;
} catch (err) {
console.log(err.message);
}
if (signtoolFound) {
let certificatesFound = false;
try {
execFileSync('signtool', [ 'sign', '/fd', 'SHA256', targetExecutable ]);
certificatesFound = true;
} catch (err) {
if (!/SignTool Error: No certificates were found that met all the given criteria/.test(err)) {
throw err;
}
}
if (certificatesFound) {
execFileSync('signtool', 'verify', '/pa', 'SHA256', targetExecutable);
}
}
}
}

module.exports = {
skipIfSingleExecutableIsNotSupported,
injectAndCodeSign,
};
Loading

0 comments on commit c4596b9

Please sign in to comment.