Repository: electron/electron
Pull request: fix: prevent log files being written to current directory on Windows

#### Description of Change

* regression was introduced in https://github.com/chromium/chromium/commit/9d2b795f72832aa7216d7c84bdbf5d2358346548
* it appears that Electron is involved in the processing of the command line for Chromium child processes but doesn't handle the new "handle" option value
* this fix duplicates the new logic in `logging_chrome.cc` introduced in the patch that caused the regression
  * that file appears to be where Electron's `logging.cc` was originally copied from based on a bunch of similarities in the code of the two files
* verified this does not write log files to the CWD with the fix

Fixes https://github.com/electron/electron/issues/46836
Fixes https://github.com/electron/electron/issues/43508

<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Contributors guide: https://github.com/electron/electron/blob/main/CONTRIBUTING.md
-->

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included and stakeholders cc'd
- [x] `npm test` passes
- [x] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md)
- [x] relevant documentation, tutorials, templates and examples are changed or added
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: fix log files written to the current working directory on Windows


Changed files:
filename: shell/common/api/electron_api_testing.cc
patch: @@ -2,8 +2,10 @@
 // Use of this source code is governed by the MIT license that can be
 // found in the LICENSE file.
 
+#include "base/command_line.h"
 #include "base/dcheck_is_on.h"
 #include "base/logging.h"
+#include "content/public/common/content_switches.h"
 #include "shell/common/gin_helper/dictionary.h"
 #include "shell/common/node_includes.h"
 #include "v8/include/v8.h"
@@ -34,12 +36,18 @@ void Log(int severity, std::string text) {
   }
 }
 
+std::string GetLoggingDestination() {
+  const auto* command_line = base::CommandLine::ForCurrentProcess();
+  return command_line->GetSwitchValueASCII(switches::kEnableLogging);
+}
+
 void Initialize(v8::Local<v8::Object> exports,
                 v8::Local<v8::Value> unused,
                 v8::Local<v8::Context> context,
                 void* priv) {
   gin_helper::Dictionary dict(context->GetIsolate(), exports);
   dict.SetMethod("log", &Log);
+  dict.SetMethod("getLoggingDestination", &GetLoggingDestination);
 }
 
 }  // namespace

filename: shell/common/logging.cc
patch: @@ -17,11 +17,41 @@
 #include "chrome/common/chrome_paths.h"
 #include "content/public/common/content_switches.h"
 
+#if BUILDFLAG(IS_WIN)
+#include <windows.h>
+#include "base/win/scoped_handle.h"
+#include "base/win/windows_handle_util.h"
+#include "sandbox/policy/switches.h"
+#endif
+
 namespace logging {
 
 constexpr base::cstring_view kLogFileName{"ELECTRON_LOG_FILE"};
 constexpr base::cstring_view kElectronEnableLogging{"ELECTRON_ENABLE_LOGGING"};
 
+#if BUILDFLAG(IS_WIN)
+base::win::ScopedHandle GetLogInheritedHandle(
+    const base::CommandLine& command_line) {
+  auto handle_str = command_line.GetSwitchValueNative(::switches::kLogFile);
+  uint32_t handle_value = 0;
+  if (!base::StringToUint(handle_str, &handle_value)) {
+    return {};
+  }
+  // Duplicate the handle from the command line so that different things can
+  // init logging. This means the handle from the parent is never closed, but
+  // there will only be one of these in the process.
+  HANDLE log_handle = nullptr;
+  if (!::DuplicateHandle(::GetCurrentProcess(),
+                         base::win::Uint32ToHandle(handle_value),
+                         ::GetCurrentProcess(), &log_handle, 0,
+                         /*bInheritHandle=*/FALSE, DUPLICATE_SAME_ACCESS)) {
+    return {};
+  }
+  // Transfer ownership to the caller.
+  return base::win::ScopedHandle(log_handle);
+}
+#endif
+
 base::FilePath GetLogFileName(const base::CommandLine& command_line) {
   std::string filename = command_line.GetSwitchValueASCII(switches::kLogFile);
   if (filename.empty())
@@ -47,9 +77,9 @@ bool HasExplicitLogFile(const base::CommandLine& command_line) {
   return !filename.empty();
 }
 
-LoggingDestination DetermineLoggingDestination(
-    const base::CommandLine& command_line,
-    bool is_preinit) {
+std::pair<LoggingDestination, bool /* filename_is_handle */>
+DetermineLoggingDestination(const base::CommandLine& command_line,
+                            bool is_preinit) {
   bool enable_logging = false;
   std::string logging_destination;
   if (command_line.HasSwitch(::switches::kEnableLogging)) {
@@ -64,7 +94,7 @@ LoggingDestination DetermineLoggingDestination(
     }
   }
   if (!enable_logging)
-    return LOG_NONE;
+    return {LOG_NONE, false};
 
   bool also_log_to_stderr = false;
 #if !defined(NDEBUG)
@@ -73,6 +103,16 @@ LoggingDestination DetermineLoggingDestination(
     also_log_to_stderr = !also_log_to_stderr_str->empty();
 #endif
 
+#if BUILDFLAG(IS_WIN)
+  if (logging_destination == "handle" &&
+      command_line.HasSwitch(::switches::kProcessType) &&
+      command_line.HasSwitch(::switches::kLogFile)) {
+    // Child processes can log to a handle duplicated from the parent, and
+    // provided in the log-file switch value.
+    return {LOG_TO_FILE, true};
+  }
+#endif  // BUILDFLAG(IS_WIN)
+
   // --enable-logging logs to stderr, --enable-logging=file logs to a file.
   // NB. this differs from Chromium, in which --enable-logging logs to a file
   // and --enable-logging=stderr logs to stderr, because that's how Electron
@@ -88,8 +128,8 @@ LoggingDestination DetermineLoggingDestination(
   // given.
   if (HasExplicitLogFile(command_line) ||
       (logging_destination == "file" && !is_preinit))
-    return LOG_TO_FILE | (also_log_to_stderr ? LOG_TO_STDERR : 0);
-  return LOG_TO_SYSTEM_DEBUG_LOG | LOG_TO_STDERR;
+    return {LOG_TO_FILE | (also_log_to_stderr ? LOG_TO_STDERR : 0), false};
+  return {LOG_TO_SYSTEM_DEBUG_LOG | LOG_TO_STDERR, false};
 }
 
 }  // namespace
@@ -98,10 +138,13 @@ void InitElectronLogging(const base::CommandLine& command_line,
                          bool is_preinit) {
   const std::string process_type =
       command_line.GetSwitchValueASCII(::switches::kProcessType);
-  LoggingDestination logging_dest =
+  auto [logging_dest, filename_is_handle] =
       DetermineLoggingDestination(command_line, is_preinit);
   LogLockingState log_locking_state = LOCK_LOG_FILE;
   base::FilePath log_path;
+#if BUILDFLAG(IS_WIN)
+  base::win::ScopedHandle log_handle;
+#endif
 
   if (command_line.HasSwitch(::switches::kLoggingLevel) &&
       GetMinLogLevel() >= 0) {
@@ -119,7 +162,19 @@ void InitElectronLogging(const base::CommandLine& command_line,
   // Don't resolve the log path unless we need to. Otherwise we leave an open
   // ALPC handle after sandbox lockdown on Windows.
   if ((logging_dest & LOG_TO_FILE) != 0) {
-    log_path = GetLogFileName(command_line);
+    if (filename_is_handle) {
+#if BUILDFLAG(IS_WIN)
+      // Child processes on Windows are provided a file handle if logging is
+      // enabled as sandboxed processes cannot open files.
+      log_handle = GetLogInheritedHandle(command_line);
+      if (!log_handle.is_valid()) {
+        LOG(ERROR) << "Unable to initialize logging from handle.";
+        return;
+      }
+#endif
+    } else {
+      log_path = GetLogFileName(command_line);
+    }
   } else {
     log_locking_state = DONT_LOCK_LOG_FILE;
   }
@@ -131,6 +186,13 @@ void InitElectronLogging(const base::CommandLine& command_line,
   LoggingSettings settings;
   settings.logging_dest = logging_dest;
   settings.log_file_path = log_path.value().c_str();
+#if BUILDFLAG(IS_WIN)
+  // Avoid initializing with INVALID_HANDLE_VALUE.
+  // This handle is owned by the logging framework and is closed when the
+  // process exits.
+  // TODO(crbug.com/328285906) Use a ScopedHandle in logging settings.
+  settings.log_file = log_handle.is_valid() ? log_handle.release() : nullptr;
+#endif
   settings.lock_log = log_locking_state;
   // If we're logging to an explicit file passed with --log-file, we don't want
   // to delete the log file on our second initialization.

filename: spec/fixtures/log-test.js
patch: @@ -0,0 +1,3 @@
+const binding = process._linkedBinding('electron_common_testing');
+binding.log(1, 'CHILD_PROCESS_TEST_LOG');
+binding.log(1, `CHILD_PROCESS_DESTINATION_${binding.getLoggingDestination()}`);

filename: spec/logging-spec.ts
patch: @@ -7,7 +7,7 @@ import { once } from 'node:events';
 import * as fs from 'node:fs/promises';
 import * as path from 'node:path';
 
-import { startRemoteControlApp, ifdescribe } from './lib/spec-helpers';
+import { startRemoteControlApp, ifdescribe, ifit } from './lib/spec-helpers';
 
 function isTestingBindingAvailable () {
   try {
@@ -127,6 +127,34 @@ ifdescribe(isTestingBindingAvailable())('logging', () => {
     expect(contents).to.match(/TEST_LOG/);
   });
 
+  ifit(process.platform === 'win32')('child process logs to the given file when --log-file is passed', async () => {
+    const logFilePath = path.join(app.getPath('temp'), 'test-log-file-' + uuid.v4());
+    const preloadPath = path.resolve(__dirname, 'fixtures', 'log-test.js');
+    const rc = await startRemoteControlApp(['--enable-logging', `--log-file=${logFilePath}`, `--boot-eval=preloadPath=${JSON.stringify(preloadPath)}`]);
+    rc.remotely(() => {
+      process._linkedBinding('electron_common_testing').log(0, 'MAIN_PROCESS_TEST_LOG');
+      const { app, BrowserWindow } = require('electron');
+      const w = new BrowserWindow({
+        show: false,
+        webPreferences: {
+          preload: preloadPath,
+          additionalArguments: ['--unsafely-expose-electron-internals-for-testing']
+        }
+      });
+      w.loadURL('about:blank');
+      w.webContents.once('did-finish-load', () => {
+        setTimeout(() => { app.quit(); });
+      });
+    });
+    await once(rc.process, 'exit');
+    const stat = await fs.stat(logFilePath);
+    expect(stat.isFile()).to.be.true();
+    const contents = await fs.readFile(logFilePath, 'utf8');
+    expect(contents).to.match(/MAIN_PROCESS_TEST_LOG/);
+    expect(contents).to.match(/CHILD_PROCESS_TEST_LOG/);
+    expect(contents).to.match(/CHILD_PROCESS_DESTINATION_handle/);
+  });
+
   it('logs to the given file when ELECTRON_LOG_FILE is set', async () => {
     const logFilePath = path.join(app.getPath('temp'), 'test-log-file-' + uuid.v4());
     const rc = await startRemoteControlApp([], { env: { ...process.env, ELECTRON_ENABLE_LOGGING: '1', ELECTRON_LOG_FILE: logFilePath } });