Browse Source

fix: utilityProcess exit codes (#42395)

* fix: utilityProcess exit codes

Co-authored-by: Shelley Vohr <[email protected]>

* chore: update patches

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <[email protected]>
Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
trop[bot] 9 months ago
parent
commit
f3cc8cb8a5

+ 1 - 0
patches/chromium/.patches

@@ -135,3 +135,4 @@ feat_add_support_for_missing_dialog_features_to_shell_dialogs.patch
 revert_fix_ime_prevent_tsf_hang_chromium_window_when_dpi_changed.patch
 fix_font_face_resolution_when_renderer_is_blocked.patch
 x11_use_localized_display_label_only_for_browser_process.patch
+feat_enable_passing_exit_code_on_service_process_crash.patch

+ 136 - 0
patches/chromium/feat_enable_passing_exit_code_on_service_process_crash.patch

@@ -0,0 +1,136 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Shelley Vohr <[email protected]>
+Date: Tue, 28 May 2024 10:44:06 +0200
+Subject: feat: enable passing exit code on service process crash
+
+This patch enables plumbing the exit code of the service process to the
+browser process when the service process crashes. The process can perform cleanup
+after the message pipe disconnection, which previously led to racy and incorrect
+exit codes in some crashing scenarios. To mitigate this, we can rely on
+ServiceProcessHost::Observer functions, but we need to pass the exit code to
+the observer.
+
+diff --git a/content/browser/service_process_host_impl.cc b/content/browser/service_process_host_impl.cc
+index 73d8c2fcbed9db89161ad3fabd5cbfb6b3761a4d..9d5673a19f4d9cc1754759ac792e0bdd0d12a2d7 100644
+--- a/content/browser/service_process_host_impl.cc
++++ b/content/browser/service_process_host_impl.cc
+@@ -72,12 +72,15 @@ class ServiceProcessTracker {
+     processes_.erase(iter);
+   }
+ 
+-  void NotifyCrashed(ServiceProcessId id) {
++  void NotifyCrashed(ServiceProcessId id, int exit_code) {
+     DCHECK_CURRENTLY_ON(BrowserThread::UI);
+     auto iter = processes_.find(id);
+     DCHECK(iter != processes_.end());
+-    for (auto& observer : observers_)
+-      observer.OnServiceProcessCrashed(iter->second.Duplicate());
++    for (auto& observer : observers_) {
++      auto params = iter->second.Duplicate();
++      params.set_exit_code(exit_code);
++      observer.OnServiceProcessCrashed(params);
++    }
+     processes_.erase(iter);
+   }
+ 
+@@ -86,6 +89,11 @@ class ServiceProcessTracker {
+     observers_.AddObserver(observer);
+   }
+ 
++  bool HasObserver(ServiceProcessHost::Observer* observer) {
++    DCHECK_CURRENTLY_ON(BrowserThread::UI);
++    return observers_.HasObserver(observer);
++  }
++
+   void RemoveObserver(ServiceProcessHost::Observer* observer) {
+     // NOTE: Some tests may remove observers after BrowserThreads are shut down.
+     DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI) ||
+@@ -153,7 +161,7 @@ class UtilityProcessClient : public UtilityProcessHost::Client {
+         process_info_->service_process_id());
+   }
+ 
+-  void OnProcessCrashed() override {
++  void OnProcessCrashed(int exit_code) override {
+     // TODO(https://crbug.com/1016027): It is unclear how we can observe
+     // |OnProcessCrashed()| without observing |OnProcessLaunched()| first, but
+     // it can happen on Android. Ignore the notification in this case.
+@@ -161,7 +169,7 @@ class UtilityProcessClient : public UtilityProcessHost::Client {
+       return;
+ 
+     GetServiceProcessTracker().NotifyCrashed(
+-        process_info_->service_process_id());
++        process_info_->service_process_id(), exit_code);
+   }
+ 
+  private:
+@@ -233,6 +241,11 @@ void ServiceProcessHost::AddObserver(Observer* observer) {
+   GetServiceProcessTracker().AddObserver(observer);
+ }
+ 
++// static
++bool ServiceProcessHost::HasObserver(Observer* observer) {
++  return GetServiceProcessTracker().HasObserver(observer);
++}
++
+ // static
+ void ServiceProcessHost::RemoveObserver(Observer* observer) {
+   GetServiceProcessTracker().RemoveObserver(observer);
+diff --git a/content/browser/utility_process_host.cc b/content/browser/utility_process_host.cc
+index 9b99a3caf366368917c39ae5c12a11f5294c3948..c54bd936ed463dea36f3b2a301549111bfded55d 100644
+--- a/content/browser/utility_process_host.cc
++++ b/content/browser/utility_process_host.cc
+@@ -551,7 +551,7 @@ void UtilityProcessHost::OnProcessCrashed(int exit_code) {
+   // Take ownership of |client_| so the destructor doesn't notify it of
+   // termination.
+   auto client = std::move(client_);
+-  client->OnProcessCrashed();
++  client->OnProcessCrashed(exit_code);
+ }
+ 
+ std::optional<std::string> UtilityProcessHost::GetServiceName() {
+diff --git a/content/browser/utility_process_host.h b/content/browser/utility_process_host.h
+index ecb0b0e02870386f3ad4365461325ba0627ba1ce..780e43ddaedbf91aea9918bb860c487b4aed54b8 100644
+--- a/content/browser/utility_process_host.h
++++ b/content/browser/utility_process_host.h
+@@ -84,7 +84,7 @@ class CONTENT_EXPORT UtilityProcessHost
+ 
+     virtual void OnProcessLaunched(const base::Process& process) {}
+     virtual void OnProcessTerminatedNormally() {}
+-    virtual void OnProcessCrashed() {}
++    virtual void OnProcessCrashed(int exit_code) {}
+   };
+ 
+   // This class is self-owned. It must be instantiated using new, and shouldn't
+diff --git a/content/public/browser/service_process_host.h b/content/public/browser/service_process_host.h
+index 4895ba5c305c898bb21472a2408ecd62afb46fd6..c67737f4647b2b07f13aee77d95401edf34c72d0 100644
+--- a/content/public/browser/service_process_host.h
++++ b/content/public/browser/service_process_host.h
+@@ -234,6 +234,10 @@ class CONTENT_EXPORT ServiceProcessHost {
+   // removed before destruction. Must be called from the UI thread only.
+   static void AddObserver(Observer* observer);
+ 
++  // Returns true if the given observer is currently registered.
++  // Must be called from the UI thread only.
++  static bool HasObserver(Observer* observer);
++
+   // Removes a registered observer. This must be called some time before
+   // |*observer| is destroyed and must be called from the UI thread only.
+   static void RemoveObserver(Observer* observer);
+diff --git a/content/public/browser/service_process_info.h b/content/public/browser/service_process_info.h
+index 1a8656aef341cd3b23af588fb00569b79d6cd100..f904af7ee6bbacf4474e0939855ecf9f2c9a5eaa 100644
+--- a/content/public/browser/service_process_info.h
++++ b/content/public/browser/service_process_info.h
+@@ -64,7 +64,13 @@ class CONTENT_EXPORT ServiceProcessInfo {
+   const std::optional<GURL>& site() const { return site_; }
+   const base::Process& GetProcess() const { return process_; }
+ 
++  void set_exit_code(int exit_code) { exit_code_ = exit_code; }
++  int exit_code() const { return exit_code_; }
++
+  private:
++  // The exit code of the process, if it has exited.
++  int exit_code_;
++
+   // The name of the service interface for which the process was launched.
+   std::string service_interface_name_;
+ 

+ 0 - 6
shell/browser/api/electron_api_app.cc

@@ -809,12 +809,6 @@ void App::BrowserChildProcessCrashedOrKilled(
   if (!data.name.empty()) {
     details.Set("name", data.name);
   }
-  if (data.process_type == content::PROCESS_TYPE_UTILITY) {
-    base::ProcessId pid = data.GetProcess().Pid();
-    auto utility_process_wrapper = UtilityProcessWrapper::FromProcessId(pid);
-    if (utility_process_wrapper)
-      utility_process_wrapper->Shutdown(info.exit_code);
-  }
   Emit("child-process-gone", details);
 }
 

+ 42 - 14
shell/browser/api/electron_api_utility_process.cc

@@ -145,6 +145,9 @@ UtilityProcessWrapper::UtilityProcessWrapper(
     }
   }
 
+  if (!content::ServiceProcessHost::HasObserver(this))
+    content::ServiceProcessHost::AddObserver(this);
+
   mojo::PendingReceiver<node::mojom::NodeService> receiver =
       node_service_remote_.BindNewPipeAndPassReceiver();
 
@@ -172,9 +175,10 @@ UtilityProcessWrapper::UtilityProcessWrapper(
                               : content::ChildProcessHost::CHILD_NORMAL)
 #endif
           .WithProcessCallback(
-              base::BindOnce(&UtilityProcessWrapper::OnServiceProcessLaunched,
+              base::BindOnce(&UtilityProcessWrapper::OnServiceProcessLaunch,
                              weak_factory_.GetWeakPtr()))
           .Pass());
+
   node_service_remote_.set_disconnect_with_reason_handler(
       base::BindOnce(&UtilityProcessWrapper::OnServiceProcessDisconnected,
                      weak_factory_.GetWeakPtr()));
@@ -210,37 +214,61 @@ UtilityProcessWrapper::UtilityProcessWrapper(
   network_context->CreateHostResolver(
       {}, host_resolver.InitWithNewPipeAndPassReceiver());
   params->host_resolver = std::move(host_resolver);
+
   node_service_remote_->Initialize(std::move(params));
 }
 
-UtilityProcessWrapper::~UtilityProcessWrapper() = default;
+UtilityProcessWrapper::~UtilityProcessWrapper() {
+  content::ServiceProcessHost::RemoveObserver(this);
+}
 
-void UtilityProcessWrapper::OnServiceProcessLaunched(
+void UtilityProcessWrapper::OnServiceProcessLaunch(
     const base::Process& process) {
   DCHECK(node_service_remote_.is_connected());
   pid_ = process.Pid();
   GetAllUtilityProcessWrappers().AddWithID(this, pid_);
-  if (stdout_read_fd_ != -1) {
+  if (stdout_read_fd_ != -1)
     EmitWithoutEvent("stdout", stdout_read_fd_);
-  }
-  if (stderr_read_fd_ != -1) {
+  if (stderr_read_fd_ != -1)
     EmitWithoutEvent("stderr", stderr_read_fd_);
-  }
-  // Emit 'spawn' event
   EmitWithoutEvent("spawn");
 }
 
-void UtilityProcessWrapper::OnServiceProcessDisconnected(
-    uint32_t error_code,
-    const std::string& description) {
+void UtilityProcessWrapper::HandleTermination(uint64_t exit_code) {
   if (pid_ != base::kNullProcessId)
     GetAllUtilityProcessWrappers().Remove(pid_);
   CloseConnectorPort();
-  // Emit 'exit' event
-  EmitWithoutEvent("exit", error_code);
+
+  EmitWithoutEvent("exit", exit_code);
+
   Unpin();
 }
 
+void UtilityProcessWrapper::OnServiceProcessDisconnected(
+    uint32_t exit_code,
+    const std::string& description) {
+  if (description == "process_exit_termination")
+    HandleTermination(exit_code);
+}
+
+void UtilityProcessWrapper::OnServiceProcessTerminatedNormally(
+    const content::ServiceProcessInfo& info) {
+  if (!info.IsService<node::mojom::NodeService>() ||
+      info.GetProcess().Pid() != pid_)
+    return;
+
+  HandleTermination(info.exit_code());
+}
+
+void UtilityProcessWrapper::OnServiceProcessCrashed(
+    const content::ServiceProcessInfo& info) {
+  if (!info.IsService<node::mojom::NodeService>() ||
+      info.GetProcess().Pid() != pid_)
+    return;
+
+  HandleTermination(info.exit_code());
+}
+
 void UtilityProcessWrapper::CloseConnectorPort() {
   if (!connector_closed_ && connector_->is_valid()) {
     host_port_.GiveDisentangledHandle(connector_->PassMessagePipe());
@@ -250,7 +278,7 @@ void UtilityProcessWrapper::CloseConnectorPort() {
   }
 }
 
-void UtilityProcessWrapper::Shutdown(int exit_code) {
+void UtilityProcessWrapper::Shutdown(uint64_t exit_code) {
   if (pid_ != base::kNullProcessId)
     GetAllUtilityProcessWrappers().Remove(pid_);
   node_service_remote_.reset();

+ 17 - 5
shell/browser/api/electron_api_utility_process.h

@@ -13,9 +13,11 @@
 #include "base/environment.h"
 #include "base/memory/weak_ptr.h"
 #include "base/process/process_handle.h"
+#include "content/public/browser/service_process_host.h"
 #include "gin/wrappable.h"
 #include "mojo/public/cpp/bindings/connector.h"
 #include "mojo/public/cpp/bindings/message.h"
+#include "mojo/public/cpp/bindings/receiver.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "shell/browser/event_emitter_mixin.h"
 #include "shell/common/gin_helper/pinnable.h"
@@ -38,7 +40,8 @@ class UtilityProcessWrapper
     : public gin::Wrappable<UtilityProcessWrapper>,
       public gin_helper::Pinnable<UtilityProcessWrapper>,
       public gin_helper::EventEmitterMixin<UtilityProcessWrapper>,
-      public mojo::MessageReceiver {
+      public mojo::MessageReceiver,
+      public content::ServiceProcessHost::Observer {
  public:
   enum class IOHandle : size_t { STDIN = 0, STDOUT = 1, STDERR = 2 };
   enum class IOType { IO_PIPE, IO_INHERIT, IO_IGNORE };
@@ -47,7 +50,7 @@ class UtilityProcessWrapper
   static gin::Handle<UtilityProcessWrapper> Create(gin::Arguments* args);
   static raw_ptr<UtilityProcessWrapper> FromProcessId(base::ProcessId pid);
 
-  void Shutdown(int exit_code);
+  void Shutdown(uint64_t exit_code);
 
   // gin::Wrappable
   static gin::WrapperInfo kWrapperInfo;
@@ -62,11 +65,11 @@ class UtilityProcessWrapper
                         base::EnvironmentMap env_map,
                         base::FilePath current_working_directory,
                         bool use_plugin_helper);
-  void OnServiceProcessDisconnected(uint32_t error_code,
-                                    const std::string& description);
-  void OnServiceProcessLaunched(const base::Process& process);
+  void OnServiceProcessLaunch(const base::Process& process);
   void CloseConnectorPort();
 
+  void HandleTermination(uint64_t exit_code);
+
   void PostMessage(gin::Arguments* args);
   bool Kill() const;
   v8::Local<v8::Value> GetOSProcessId(v8::Isolate* isolate) const;
@@ -74,6 +77,15 @@ class UtilityProcessWrapper
   // mojo::MessageReceiver
   bool Accept(mojo::Message* mojo_message) override;
 
+  // content::ServiceProcessHost::Observer
+  void OnServiceProcessTerminatedNormally(
+      const content::ServiceProcessInfo& info) override;
+  void OnServiceProcessCrashed(
+      const content::ServiceProcessInfo& info) override;
+
+  void OnServiceProcessDisconnected(uint32_t exit_code,
+                                    const std::string& description);
+
   base::ProcessId pid_ = base::kNullProcessId;
 #if BUILDFLAG(IS_WIN)
   // Non-owning handles, these will be closed when the

+ 8 - 5
shell/common/gin_helper/event_emitter_caller.cc

@@ -13,29 +13,32 @@ v8::Local<v8::Value> CallMethodWithArgs(v8::Isolate* isolate,
                                         v8::Local<v8::Object> obj,
                                         const char* method,
                                         ValueVector* args) {
-  // Only set up the node::CallbackScope if there's a node environment.
+  // An active node::Environment is required for node::MakeCallback.
   std::unique_ptr<node::CallbackScope> callback_scope;
   if (node::Environment::GetCurrent(isolate)) {
     v8::HandleScope handle_scope(isolate);
     callback_scope = std::make_unique<node::CallbackScope>(
         isolate, v8::Object::New(isolate), node::async_context{0, 0});
+  } else {
+    return v8::Boolean::New(isolate, false);
   }
 
   // Perform microtask checkpoint after running JavaScript.
   gin_helper::MicrotasksScope microtasks_scope(
       isolate, obj->GetCreationContextChecked()->GetMicrotaskQueue(), true);
-  // Use node::MakeCallback to call the callback, and it will also run pending
-  // tasks in Node.js.
+
+  // node::MakeCallback will also run pending tasks in Node.js.
   v8::MaybeLocal<v8::Value> ret = node::MakeCallback(
       isolate, obj, method, args->size(), args->data(), {0, 0});
+
   // If the JS function throws an exception (doesn't return a value) the result
   // of MakeCallback will be empty and therefore ToLocal will be false, in this
   // case we need to return "false" as that indicates that the event emitter did
   // not handle the event
   v8::Local<v8::Value> localRet;
-  if (ret.ToLocal(&localRet)) {
+  if (ret.ToLocal(&localRet))
     return localRet;
-  }
+
   return v8::Boolean::New(isolate, false);
 }
 

+ 1 - 1
shell/services/node/node_service.cc

@@ -104,7 +104,7 @@ void NodeService::Initialize(node::mojom::NodeServiceParamsPtr params) {
         js_env_->DestroyMicrotasksRunner();
         node::Stop(env, node::StopFlags::kDoNotTerminateIsolate);
         node_env_stopped_ = true;
-        receiver_.ResetWithReason(exit_code, "");
+        receiver_.ResetWithReason(exit_code, "process_exit_termination");
       });
 
   node_env_->set_trace_sync_io(node_env_->options()->trace_sync_io);

+ 21 - 3
spec/api-utility-process-spec.ts

@@ -60,11 +60,29 @@ describe('utilityProcess module', () => {
       expect(code).to.equal(0);
     });
 
+    ifit(!isWindows32Bit)('emits the correct error code when child process exits nonzero', async () => {
+      const child = utilityProcess.fork(path.join(fixturesPath, 'empty.js'));
+      await once(child, 'spawn');
+      const exit = once(child, 'exit');
+      process.kill(child.pid!);
+      const [code] = await exit;
+      expect(code).to.not.equal(0);
+    });
+
+    ifit(!isWindows32Bit)('emits the correct error code when child process is killed', async () => {
+      const child = utilityProcess.fork(path.join(fixturesPath, 'empty.js'));
+      await once(child, 'spawn');
+      const exit = once(child, 'exit');
+      process.kill(child.pid!);
+      const [code] = await exit;
+      expect(code).to.not.equal(0);
+    });
+
     ifit(!isWindows32Bit)('emits \'exit\' when child process crashes', async () => {
       const child = utilityProcess.fork(path.join(fixturesPath, 'crash.js'));
-      // Do not check for exit code in this case,
-      // SIGSEGV code can be 139 or 11 across our different CI pipeline.
-      await once(child, 'exit');
+      // SIGSEGV code can differ across pipelines but should never be 0.
+      const [code] = await once(child, 'exit');
+      expect(code).to.not.equal(0);
     });
 
     ifit(!isWindows32Bit)('emits \'exit\' corresponding to the child process', async () => {