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

sea: add option to disable the experimental SEA warning #47588

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4b66620
sea: add option to disable the experimental SEA warning
RaisinTen Apr 14, 2023
e69de9f
src: use string_views in JSONParser
RaisinTen Apr 18, 2023
ec57d90
src: proper Maybe usage in JSONParser
RaisinTen Apr 18, 2023
4442309
src: add TODO for adding support for non-ASCII encoded string fields
RaisinTen Apr 18, 2023
f49d7ef
doc: use `disableExperimentalSEAWarning` in SEA example
RaisinTen Apr 24, 2023
ec0cdd6
sea: use a uint32_t bit field instead of bool disable_experimental_se…
RaisinTen Apr 25, 2023
a4af172
sea: use std::bitset instead of uint32_t
RaisinTen Apr 25, 2023
41669e4
fixup! sea: use std::bitset instead of uint32_t
RaisinTen Apr 25, 2023
2fd9f9a
fixup! fixup! sea: use std::bitset instead of uint32_t
RaisinTen Apr 25, 2023
8bb2fed
sea: use the SeaFlags enum class instead of std::bitset
RaisinTen Apr 26, 2023
457dfcd
sea: move flags before code in SeaResource struct
RaisinTen Apr 26, 2023
ff0a9c7
sea: expose only isExperimentalSeaWarningNeeded to JS from the binding
RaisinTen Apr 26, 2023
8d67348
doc: omit false option from disableExperimentalSEAWarning doc
RaisinTen Apr 26, 2023
5096a28
doc: omit disableExperimentalSEAWarning from the summary
RaisinTen Apr 26, 2023
5ce2126
fixup! sea: expose only isExperimentalSeaWarningNeeded to JS from the…
RaisinTen Apr 26, 2023
0ff4075
sea: check IsSingleExecutable value before calling FindSingleExecutab…
RaisinTen Apr 27, 2023
305b7a6
src: support UTF8 fields in JSONParser
RaisinTen Apr 27, 2023
c2da60b
src: do proper Maybe usage
RaisinTen Apr 27, 2023
eb858c1
test: move single executable E2E tests to sequential
RaisinTen Apr 27, 2023
b3f437a
test: convert the SEA skips into a helper in test/common
RaisinTen Apr 28, 2023
83f996b
fixup! test: convert the SEA skips into a helper in test/common
RaisinTen Apr 28, 2023
5d21e60
Revert "src: do proper Maybe usage"
RaisinTen Apr 29, 2023
009fe2f
src: define kHeaderSize and use it everywhere
RaisinTen May 3, 2023
d6642b7
test: wrap inject + code signing part into a helper
RaisinTen May 3, 2023
3373bd5
fixup! test: wrap inject + code signing part into a helper
RaisinTen May 3, 2023
2b5804e
fixup! src: define kHeaderSize and use it everywhere
RaisinTen May 3, 2023
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 .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,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 @@ -128,7 +128,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)) {
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
return {};
}
if (!has_field) {
return false;
}
RaisinTen marked this conversation as resolved.
Show resolved Hide resolved
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;
RaisinTen marked this conversation as resolved.
Show resolved Hide resolved
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