Skip to content
Merged
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
3 changes: 3 additions & 0 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -3806,6 +3806,8 @@ behavior. For XOF hash functions such as `'shake256'`, the `outputLength` option
specifies the desired output length in bytes. It is required for XOF hash
functions without a default output length.

When the data is small (< 5MB) and readily available, [`crypto.hash()`][] is usually faster.

The `algorithm` is dependent on the available algorithms supported by the
version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc.
On recent releases of OpenSSL, `openssl list -digest-algorithms` will
Expand Down Expand Up @@ -7018,6 +7020,7 @@ See the [list of SSL OP Flags][] for details.
[`crypto.getCurves()`]: #cryptogetcurves
[`crypto.getDiffieHellman()`]: #cryptogetdiffiehellmangroupname
[`crypto.getHashes()`]: #cryptogethashes
[`crypto.hash()`]: #cryptohashalgorithm-data-options
[`crypto.privateDecrypt()`]: #cryptoprivatedecryptprivatekey-buffer
[`crypto.privateEncrypt()`]: #cryptoprivateencryptprivatekey-buffer
[`crypto.publicDecrypt()`]: #cryptopublicdecryptkey-buffer
Expand Down
8 changes: 4 additions & 4 deletions doc/api/single-executable-applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ binary.
* On macOS:

```bash
codesign --sign - hello
codesign --sign - sea
```

* On Windows (optional):
Expand All @@ -80,22 +80,22 @@ binary.
binary would still be runnable.

```powershell
signtool sign /fd SHA256 hello.exe
signtool sign /fd SHA256 sea.exe
```

5. Run the binary:

* On systems other than Windows

```console
$ ./hello world
$ ./sea world
Hello, world!
```

* On Windows

```console
$ .\hello.exe world
$ .\sea.exe world
Hello, world!
```

Expand Down
6 changes: 4 additions & 2 deletions doc/api/vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,7 @@ const contextifiedObject = vm.createContext({ secret: 42 });
const module = new vm.SourceTextModule(
'Object.getPrototypeOf(import.meta.prop).secret = secret;',
{
context: contextifiedObject,
initializeImportMeta(meta) {
// Note: this object is created in the top context. As such,
// Object.getPrototypeOf(import.meta.prop) points to the
Expand All @@ -937,7 +938,7 @@ await module.evaluate();
// To fix this problem, replace
// meta.prop = {};
// above with
// meta.prop = vm.runInContext('{}', contextifiedObject);
// meta.prop = vm.runInContext('({})', contextifiedObject);
```

```cjs
Expand All @@ -947,6 +948,7 @@ const contextifiedObject = vm.createContext({ secret: 42 });
const module = new vm.SourceTextModule(
'Object.getPrototypeOf(import.meta.prop).secret = secret;',
{
context: contextifiedObject,
initializeImportMeta(meta) {
// Note: this object is created in the top context. As such,
// Object.getPrototypeOf(import.meta.prop) points to the
Expand All @@ -964,7 +966,7 @@ const contextifiedObject = vm.createContext({ secret: 42 });
// To fix this problem, replace
// meta.prop = {};
// above with
// meta.prop = vm.runInContext('{}', contextifiedObject);
// meta.prop = vm.runInContext('({})', contextifiedObject);
})();
```

Expand Down
105 changes: 60 additions & 45 deletions lib/internal/vfs/providers/memory.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const {
ArrayFrom,
ArrayPrototypePop,
ArrayPrototypePush,
DateNow,
SafeMap,
Expand Down Expand Up @@ -613,59 +614,73 @@ class MemoryProvider extends VirtualProvider {
*/
#readdirRecursive(dirEntry, dirPath, withFileTypes) {
const results = [];
// Directories on the current traversal path. A directory reached again
// through a symlink cycle is not descended into (but is still listed).
const active = new SafeSet();

const walk = (entry, currentPath, relativePath) => {
if (active.has(entry)) {
return;
// Traverse depth-first with an explicit stack instead of recursion, so a
// deeply nested tree cannot exhaust the call stack. Each frame is a
// directory being walked together with a snapshot of its children and the
// index of the next child to visit.
const enter = (entry, currentPath, relativePath) => {
this.#ensurePopulated(entry, currentPath);
active.add(entry);
ArrayPrototypePush(stack, {
entry,
currentPath,
relativePath,
children: ArrayFrom(entry.children),
index: 0,
});
};

const stack = [];
enter(dirEntry, dirPath, '');

while (stack.length > 0) {
const frame = stack[stack.length - 1];
if (frame.index >= frame.children.length) {
active.delete(frame.entry);
ArrayPrototypePop(stack);
continue;
}

active.add(entry);
try {
this.#ensurePopulated(entry, currentPath);

for (const { 0: name, 1: childEntry } of entry.children) {
const childRelative = relativePath ?
relativePath + '/' + name : name;

if (withFileTypes) {
let type;
if (childEntry.isSymbolicLink()) {
type = UV_DIRENT_LINK;
} else if (childEntry.isDirectory()) {
type = UV_DIRENT_DIR;
} else {
type = UV_DIRENT_FILE;
}
ArrayPrototypePush(results,
new Dirent(childRelative, type, dirPath));
} else {
ArrayPrototypePush(results, childRelative);
}
const { 0: name, 1: childEntry } = frame.children[frame.index++];
const childRelative = frame.relativePath ?
frame.relativePath + '/' + name : name;

// Follow symlinks to directories for recursive traversal.
// Track the active traversal path to avoid symlink cycles.
let resolvedChild = childEntry;
if (childEntry.isSymbolicLink()) {
const targetPath = this.#resolveSymlinkTarget(
pathPosix.join(currentPath, name), childEntry.target,
);
const result = this.#lookupEntry(targetPath, true, 0);
if (result.entry) {
resolvedChild = result.entry;
}
}
if (resolvedChild.isDirectory()) {
const childPath = pathPosix.join(currentPath, name);
walk(resolvedChild, childPath, childRelative);
}
if (withFileTypes) {
let type;
if (childEntry.isSymbolicLink()) {
type = UV_DIRENT_LINK;
} else if (childEntry.isDirectory()) {
type = UV_DIRENT_DIR;
} else {
type = UV_DIRENT_FILE;
}
} finally {
active.delete(entry);
ArrayPrototypePush(results, new Dirent(childRelative, type, dirPath));
} else {
ArrayPrototypePush(results, childRelative);
}
};

walk(dirEntry, dirPath, '');
// Follow symlinks to directories for recursive traversal, skipping any
// directory already on the active path to avoid symlink cycles.
let resolvedChild = childEntry;
if (childEntry.isSymbolicLink()) {
const targetPath = this.#resolveSymlinkTarget(
pathPosix.join(frame.currentPath, name), childEntry.target,
);
const result = this.#lookupEntry(targetPath, true, 0);
if (result.entry) {
resolvedChild = result.entry;
}
}
if (resolvedChild.isDirectory() && !active.has(resolvedChild)) {
enter(resolvedChild, pathPosix.join(frame.currentPath, name),
childRelative);
}
}

return results;
}

Expand Down
29 changes: 29 additions & 0 deletions src/node_zlib.cc
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ class ZstdDecompressContext final : public ZstdContext {
// Streaming-related, should be available for all compression libraries:
void Close();
void DoThreadPoolWork();
CompressionError GetErrorInfo() const;
CompressionError ResetStream();

// Zstd specific:
Expand All @@ -375,6 +376,7 @@ class ZstdDecompressContext final : public ZstdContext {

private:
DeleteFnPtr<ZSTD_DCtx, ZstdDecompressContext::FreeZstd> dctx_;
bool frame_complete_ = false;
};

class CompressionStreamMemoryOwner {
Expand Down Expand Up @@ -1717,6 +1719,8 @@ void ZstdDecompressContext::Close() {

CompressionError ZstdDecompressContext::Init(uint64_t pledged_src_size,
std::string_view dictionary) {
frame_complete_ = false;

#ifdef NODE_BUNDLED_ZSTD
ZSTD_customMem custom_mem = {
CompressionStreamMemoryOwner::AllocForBrotli,
Expand Down Expand Up @@ -1752,12 +1756,37 @@ CompressionError ZstdDecompressContext::ResetStream() {
}

void ZstdDecompressContext::DoThreadPoolWork() {
// The JavaScript processing loop retries with an empty input buffer when the
// previous call filled the output buffer. Avoid interpreting that retry as
// the beginning of a new, incomplete frame.
if (frame_complete_ && input_.size == 0) {
return;
}

size_t const ret = ZSTD_decompressStream(dctx_.get(), &output_, &input_);
if (ZSTD_isError(ret)) {
frame_complete_ = false;
error_ = ZSTD_getErrorCode(ret);
error_code_string_ = ZstdStrerror(error_);
error_string_ = ZSTD_getErrorString(error_);
} else {
frame_complete_ = ret == 0;
}
}

CompressionError ZstdDecompressContext::GetErrorInfo() const {
CompressionError error = ZstdContext::GetErrorInfo();
if (error.IsError()) {
return error;
}

if (flush_ == ZSTD_e_end && !frame_complete_ && input_.pos == input_.size &&
output_.pos < output_.size) {
return CompressionError(
"unexpected end of file", "Z_BUF_ERROR", Z_BUF_ERROR);
}

return {};
}

template <typename Stream>
Expand Down
33 changes: 20 additions & 13 deletions src/permission/permission.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ namespace permission {
const auto resource__ = (resource); \
if (!env__->permission()->is_granted(env__, perm__, resource__)) \
[[unlikely]] { \
node::permission::Permission::ThrowAccessDenied( \
env__, perm__, resource__); \
if (!env__->permission()->warning_only()) return __VA_ARGS__; \
if (!env__->permission()->warning_only()) { \
node::permission::Permission::ThrowAccessDenied( \
env__, perm__, resource__); \
return __VA_ARGS__; \
} \
} \
} while (0)

Expand All @@ -51,9 +53,11 @@ namespace permission {
const auto resource__ = (resource); \
if (!env__->permission()->is_granted(env__, perm__, resource__)) \
[[unlikely]] { \
node::permission::Permission::AsyncThrowAccessDenied( \
env__, (wrap), perm__, resource__); \
if (!env__->permission()->warning_only()) return __VA_ARGS__; \
if (!env__->permission()->warning_only()) { \
node::permission::Permission::AsyncThrowAccessDenied( \
env__, (wrap), perm__, resource__); \
return __VA_ARGS__; \
} \
} \
} while (0)

Expand All @@ -65,14 +69,17 @@ namespace permission {
const auto resource__ = (resource); \
if (!env__->permission()->is_granted(env__, perm__, resource__)) \
[[unlikely]] { \
Local<Value> err_access; \
if (node::permission::CreateAccessDeniedError(env__, perm__, resource__) \
.ToLocal(&err_access)) { \
args.GetReturnValue().Set(err_access); \
} else { \
args.GetReturnValue().Set(UV_EACCES); \
if (!env__->permission()->warning_only()) { \
Local<Value> err_access; \
if (node::permission::CreateAccessDeniedError( \
env__, perm__, resource__) \
.ToLocal(&err_access)) { \
args.GetReturnValue().Set(err_access); \
} else { \
args.GetReturnValue().Set(UV_EACCES); \
} \
return __VA_ARGS__; \
} \
return __VA_ARGS__; \
} \
} while (0)

Expand Down
59 changes: 59 additions & 0 deletions test/parallel/test-permission-audit-fs-does-not-deny.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use strict';

const common = require('../common');
const { isMainThread } = require('worker_threads');

if (!isMainThread) {
common.skip('This test only works on a main thread');
}

const assert = require('assert');
const { spawnSync } = require('child_process');
const { test } = require('node:test');
const fixtures = require('../common/fixtures');

const blockedFile = fixtures.path('permission', 'deny', 'protected-file.md');

function runAudit(mode) {
const childScript = `
const dc = require('node:diagnostics_channel');
const msgs = [];
dc.subscribe('node:permission-model:fs', (m) => msgs.push({
permission: m.permission,
resource: m.resource,
}));
try {
${mode === 'eval' ?
`eval('require("node:fs").readFileSync(process.env.BLOCKED_FILE)');` :
`require('node:fs').readFileSync(process.env.BLOCKED_FILE);`}
console.log('RESULT NO_THROW');
} catch (e) {
console.log('RESULT THREW ' + e.code);
}
console.log('AUDIT ' + JSON.stringify(msgs));
`;

const env = { ...process.env, BLOCKED_FILE: blockedFile };
const { status, stdout, stderr } = spawnSync(
process.execPath,
['--permission-audit', '-e', childScript],
{ encoding: 'utf8', env },
);
assert.strictEqual(status, 0, stderr);
const lines = stdout.split('\n');
assert.ok(lines.includes('RESULT NO_THROW'), stdout);
const auditLine = lines.find((l) => l.startsWith('AUDIT '));
assert.ok(auditLine, stdout);
const msgs = JSON.parse(auditLine.replace('AUDIT ', ''));
assert.strictEqual(msgs.length, 1);
assert.strictEqual(msgs[0].permission, 'FileSystemRead');
assert.ok(msgs[0].resource.endsWith('protected-file.md'));
}

test('permission-audit logs fs denial without throwing', () => {
runAudit('direct');
});

test('permission-audit logs fs denial without throwing (eval)', () => {
runAudit('eval');
});
Loading
Loading