|
11 | 11 | #include "node_profiling.h" |
12 | 12 | #include "node_snapshot_builder.h" |
13 | 13 | #include "permission/permission.h" |
| 14 | +#include "path.h" |
14 | 15 | #include "util-inl.h" |
15 | 16 | #include "v8-cppgc.h" |
16 | 17 | #include "v8-profiler.h" |
@@ -190,6 +191,8 @@ class WorkerThreadData { |
190 | 191 | return; |
191 | 192 | } |
192 | 193 |
|
| 194 | + SetIsolateUpForNode(isolate); |
| 195 | + |
193 | 196 | // Be sure it's called before Environment::InitializeDiagnostics() |
194 | 197 | // so that this callback stays when the callback of |
195 | 198 | // --heapsnapshot-near-heap-limit gets is popped. |
@@ -278,7 +281,6 @@ size_t Worker::NearHeapLimit(void* data, size_t current_heap_limit, |
278 | 281 | Environment* env = worker->env(); |
279 | 282 | if (env != nullptr) { |
280 | 283 | DCHECK(!env->is_in_heapsnapshot_heap_limit_callback()); |
281 | | - DCHECK(!env->is_in_heap_profile_near_heap_limit_callback()); |
282 | 284 | Debug(env, |
283 | 285 | DebugCategory::DIAGNOSTICS, |
284 | 286 | "Throwing ERR_WORKER_OUT_OF_MEMORY, " |
@@ -503,6 +505,245 @@ Worker::~Worker() { |
503 | 505 | Debug(this, "Worker %llu destroyed", thread_id_.id); |
504 | 506 | } |
505 | 507 |
|
| 508 | + |
| 509 | + |
| 510 | +// SEMVER-MAJOR: Permission ceiling for Worker when execArgv is explicit |
| 511 | +// (including []). Default Worker (no execArgv) is unchanged. |
| 512 | +// |
| 513 | +// After options parse, NODE_OPTIONS and repeated --allow-* are already in |
| 514 | +// EnvironmentOptions. Runtime FSPermission remains authoritative for FS checks; |
| 515 | +// path filtering here is create-time only (prefix / exact / *). |
| 516 | +// |
| 517 | +// Boolean --allow-* dimensions are listed once in PERMISSION_BOOL_FLAGS so |
| 518 | +// ceiling / intersect / CLI token / rebuild cannot drift. |
| 519 | + |
| 520 | +namespace { |
| 521 | + |
| 522 | +// Single source of truth for boolean permission dimensions (not fs path lists). |
| 523 | +#define PERMISSION_BOOL_FLAGS(V) \ |
| 524 | + V(allow_addons, "--allow-addons") \ |
| 525 | + V(allow_inspector, "--allow-inspector") \ |
| 526 | + V(allow_child_process, "--allow-child-process") \ |
| 527 | + V(allow_net, "--allow-net") \ |
| 528 | + V(allow_wasi, "--allow-wasi") \ |
| 529 | + V(allow_ffi, "--allow-ffi") \ |
| 530 | + V(allow_openssl_store, "--allow-openssl-store") \ |
| 531 | + V(allow_worker_threads, "--allow-worker") |
| 532 | + |
| 533 | +bool WorkerConfiguredPermission(const EnvironmentOptions* w) { |
| 534 | + if (w == nullptr) return false; |
| 535 | + if (w->permission || w->permission_audit) return true; |
| 536 | + if (!w->allow_fs_read.empty() || !w->allow_fs_write.empty()) return true; |
| 537 | +#define V(field, flag) || w->field |
| 538 | + return false PERMISSION_BOOL_FLAGS(V); |
| 539 | +#undef V |
| 540 | +} |
| 541 | + |
| 542 | +void ApplyParentPermissionCeiling(EnvironmentOptions* w, |
| 543 | + const EnvironmentOptions* parent) { |
| 544 | + w->permission = true; |
| 545 | + w->permission_audit = parent->permission_audit; |
| 546 | +#define V(field, flag) w->field = parent->field; |
| 547 | + PERMISSION_BOOL_FLAGS(V) |
| 548 | +#undef V |
| 549 | + w->allow_fs_read = parent->allow_fs_read; |
| 550 | + w->allow_fs_write = parent->allow_fs_write; |
| 551 | +} |
| 552 | + |
| 553 | +void NormalizePathForCompare(std::string* s) { |
| 554 | + while (s->size() > 1 && |
| 555 | + (s->back() == '/' || s->back() == static_cast<char>(92))) { |
| 556 | + s->pop_back(); |
| 557 | + } |
| 558 | +#ifdef _WIN32 |
| 559 | + for (char& c : *s) { |
| 560 | + if (c >= 'A' && c <= 'Z') { |
| 561 | + c = static_cast<char>(c - 'A' + 'a'); |
| 562 | + } |
| 563 | + if (c == '/') c = static_cast<char>(92); |
| 564 | + } |
| 565 | +#endif |
| 566 | +} |
| 567 | + |
| 568 | +std::string ResolveForCompare(Environment* env, const std::string& in) { |
| 569 | + if (in.empty() || in == "*") return in; |
| 570 | + std::string resolved = |
| 571 | + PathResolve(env, std::vector<std::string_view>{std::string_view(in)}); |
| 572 | + if (resolved.empty()) resolved = in; |
| 573 | + NormalizePathForCompare(&resolved); |
| 574 | + return resolved; |
| 575 | +} |
| 576 | + |
| 577 | +// parent_raw may be raw; resolved_requested is already ResolveForCompare'd. |
| 578 | +bool ParentEntryCoversResolvedPath(Environment* env, |
| 579 | + const std::string& parent_raw, |
| 580 | + const std::string& resolved_requested) { |
| 581 | + if (parent_raw == "*") return true; |
| 582 | + const std::string parent = ResolveForCompare(env, parent_raw); |
| 583 | + if (parent.empty()) return false; |
| 584 | + if (resolved_requested == parent) return true; |
| 585 | + if (resolved_requested.size() <= parent.size()) return false; |
| 586 | + if (resolved_requested.compare(0, parent.size(), parent) != 0) return false; |
| 587 | + const char next = resolved_requested[parent.size()]; |
| 588 | + return next == '/' || next == static_cast<char>(92); |
| 589 | +} |
| 590 | + |
| 591 | +bool ParentListHasWildcard(const std::vector<std::string>& parent) { |
| 592 | + for (const std::string& entry : parent) { |
| 593 | + if (entry == "*") return true; |
| 594 | + } |
| 595 | + return false; |
| 596 | +} |
| 597 | + |
| 598 | +void FilterPathListToParentSubset(Environment* env, |
| 599 | + EnvironmentOptions* w, |
| 600 | + std::vector<std::string>* worker, |
| 601 | + const std::vector<std::string>& parent) { |
| 602 | + if (worker == nullptr) return; |
| 603 | + |
| 604 | + // Worker listed no fs paths → keep empty (restrict). |
| 605 | + // Callers set w->permission = true before this runs. |
| 606 | + if (worker->empty()) { |
| 607 | + return; |
| 608 | + } |
| 609 | + |
| 610 | + // Parent "*" → FS already unrestricted; worker paths cannot exceed parent. |
| 611 | + if (ParentListHasWildcard(parent)) return; |
| 612 | + |
| 613 | + std::vector<std::string> out; |
| 614 | + out.reserve(worker->size()); |
| 615 | + bool saw_star = false; |
| 616 | + for (const std::string& wpath : *worker) { |
| 617 | + if (wpath == "*") { |
| 618 | + saw_star = true; |
| 619 | + continue; |
| 620 | + } |
| 621 | + const std::string resolved_wpath = ResolveForCompare(env, wpath); |
| 622 | + for (const std::string& entry : parent) { |
| 623 | + if (ParentEntryCoversResolvedPath(env, entry, resolved_wpath)) { |
| 624 | + out.push_back(resolved_wpath); |
| 625 | + break; |
| 626 | + } |
| 627 | + } |
| 628 | + } |
| 629 | + // "*" alone or with concrete paths → full parent list (not a subset). |
| 630 | + if (saw_star) { |
| 631 | + *worker = parent; |
| 632 | + return; |
| 633 | + } |
| 634 | + *worker = std::move(out); |
| 635 | +} |
| 636 | + |
| 637 | +void IntersectPermissionGrants(Environment* env, |
| 638 | + EnvironmentOptions* w, |
| 639 | + const EnvironmentOptions* parent) { |
| 640 | + w->permission = true; |
| 641 | + w->permission_audit = w->permission_audit || parent->permission_audit; |
| 642 | +#define V(field, flag) w->field = w->field && parent->field; |
| 643 | + PERMISSION_BOOL_FLAGS(V) |
| 644 | +#undef V |
| 645 | + FilterPathListToParentSubset(env, w, &w->allow_fs_read, parent->allow_fs_read); |
| 646 | + FilterPathListToParentSubset( |
| 647 | + env, w, &w->allow_fs_write, parent->allow_fs_write); |
| 648 | +} |
| 649 | + |
| 650 | +void ClampWorkerPermissionToParent(Environment* env, |
| 651 | + PerIsolateOptions* worker_opts) { |
| 652 | + if (worker_opts == nullptr || env == nullptr || |
| 653 | + !env->permission()->enabled()) { |
| 654 | + return; |
| 655 | + } |
| 656 | + EnvironmentOptions* parent = |
| 657 | + env->isolate_data()->options()->get_per_env_options(); |
| 658 | + EnvironmentOptions* w = worker_opts->get_per_env_options(); |
| 659 | + if (parent == nullptr || w == nullptr) return; |
| 660 | + |
| 661 | + if (!WorkerConfiguredPermission(w)) { |
| 662 | + ApplyParentPermissionCeiling(w, parent); |
| 663 | + } else { |
| 664 | + IntersectPermissionGrants(env, w, parent); |
| 665 | + } |
| 666 | +} |
| 667 | + |
| 668 | +bool IsPermissionCliToken(const std::string& a) { |
| 669 | + if (a == "--permission" || a == "--permission-audit") return true; |
| 670 | + if (a == "--allow-fs-read" || a == "--allow-fs-write") return true; |
| 671 | + if (a.rfind("--allow-fs-read=", 0) == 0) return true; |
| 672 | + if (a.rfind("--allow-fs-write=", 0) == 0) return true; |
| 673 | +#define V(field, flag) \ |
| 674 | + if (a == flag) return true; \ |
| 675 | + { \ |
| 676 | + const size_t n = sizeof(flag) - 1; \ |
| 677 | + if (a.size() > n && a.compare(0, n, flag) == 0 && a[n] == '=') \ |
| 678 | + return true; \ |
| 679 | + } |
| 680 | + PERMISSION_BOOL_FLAGS(V) |
| 681 | +#undef V |
| 682 | + return false; |
| 683 | +} |
| 684 | + |
| 685 | +bool PermissionFlagTakesNextArg(const std::string& a) { |
| 686 | + return a == "--allow-fs-read" || a == "--allow-fs-write"; |
| 687 | +} |
| 688 | + |
| 689 | +bool PathSafeForAllowFlag(const std::string& path) { |
| 690 | + if (path.empty()) return false; |
| 691 | + for (unsigned char c : path) { |
| 692 | + if (c == 0 || c == 10 || c == 13) return false; |
| 693 | + } |
| 694 | + return true; |
| 695 | +} |
| 696 | + |
| 697 | +void RebuildExecArgvOutFromPermissionOptions( |
| 698 | + PerIsolateOptions* worker_opts, std::vector<std::string>* exec_argv_out) { |
| 699 | + if (worker_opts == nullptr || exec_argv_out == nullptr) return; |
| 700 | + EnvironmentOptions* w = worker_opts->get_per_env_options(); |
| 701 | + if (w == nullptr || !w->permission) return; |
| 702 | + |
| 703 | + std::vector<std::string> kept; |
| 704 | + kept.reserve(exec_argv_out->size()); |
| 705 | + for (size_t i = 0; i < exec_argv_out->size(); ++i) { |
| 706 | + const std::string& tok = (*exec_argv_out)[i]; |
| 707 | + if (tok.empty()) continue; |
| 708 | + if (IsPermissionCliToken(tok)) { |
| 709 | + // Space-form --allow-fs-read/--allow-fs-write always consume next token |
| 710 | + // (paths may start with '-'). |
| 711 | + if (PermissionFlagTakesNextArg(tok) && i + 1 < exec_argv_out->size()) { |
| 712 | + ++i; |
| 713 | + } |
| 714 | + continue; |
| 715 | + } |
| 716 | + kept.push_back(tok); |
| 717 | + } |
| 718 | + |
| 719 | + std::vector<std::string> out; |
| 720 | + out.reserve(kept.size() + 16 + w->allow_fs_read.size() + |
| 721 | + w->allow_fs_write.size()); |
| 722 | + for (const std::string& tok : kept) out.push_back(tok); |
| 723 | + |
| 724 | + out.push_back("--permission"); |
| 725 | + if (w->permission_audit) out.push_back("--permission-audit"); |
| 726 | +#define V(field, flag) \ |
| 727 | + if (w->field) out.push_back(flag); |
| 728 | + PERMISSION_BOOL_FLAGS(V) |
| 729 | +#undef V |
| 730 | + for (const std::string& p : w->allow_fs_read) { |
| 731 | + if (!PathSafeForAllowFlag(p)) continue; |
| 732 | + out.push_back("--allow-fs-read=" + p); |
| 733 | + } |
| 734 | + for (const std::string& p : w->allow_fs_write) { |
| 735 | + if (!PathSafeForAllowFlag(p)) continue; |
| 736 | + out.push_back("--allow-fs-write=" + p); |
| 737 | + } |
| 738 | + *exec_argv_out = std::move(out); |
| 739 | +} |
| 740 | + |
| 741 | +#undef PERMISSION_BOOL_FLAGS |
| 742 | + |
| 743 | +} // namespace |
| 744 | + |
| 745 | + |
| 746 | + |
506 | 747 | void Worker::New(const FunctionCallbackInfo<Value>& args) { |
507 | 748 | Environment* env = Environment::GetCurrent(args); |
508 | 749 | THROW_IF_INSUFFICIENT_PERMISSIONS( |
@@ -682,6 +923,16 @@ void Worker::New(const FunctionCallbackInfo<Value>& args) { |
682 | 923 | per_isolate_opts = env->isolate_data()->options()->Clone(); |
683 | 924 | } |
684 | 925 |
|
| 926 | + // |
| 927 | + |
| 928 | + // Explicit execArgv only (including []). Default Worker path unchanged. |
| 929 | + if (env->permission()->enabled() && per_isolate_opts && |
| 930 | + args[2]->IsArray()) { |
| 931 | + ClampWorkerPermissionToParent(env, per_isolate_opts.get()); |
| 932 | + RebuildExecArgvOutFromPermissionOptions(per_isolate_opts.get(), |
| 933 | + &exec_argv_out); |
| 934 | + } |
| 935 | + |
685 | 936 | // Internal workers should not wait for inspector frontend to connect or |
686 | 937 | // break on the first line of internal scripts. Module loader threads are |
687 | 938 | // essential to load user codes and must not be blocked by the inspector |
@@ -1108,9 +1359,6 @@ void Worker::StopHeapProfile(const FunctionCallbackInfo<Value>& args) { |
1108 | 1359 | std::ostringstream out_stream; |
1109 | 1360 | bool success = |
1110 | 1361 | node::SerializeHeapProfile(worker_env->isolate(), out_stream); |
1111 | | - if (success) { |
1112 | | - worker_env->isolate()->GetHeapProfiler()->StopSamplingHeapProfiler(); |
1113 | | - } |
1114 | 1362 | env->SetImmediateThreadsafe( |
1115 | 1363 | [taker = std::move(taker), |
1116 | 1364 | out_stream = std::move(out_stream), |
|
0 commit comments