Skip to content

Ghost bug#24

Merged
AboMeezO merged 3 commits into
mainfrom
GhostBug
Mar 18, 2026
Merged

Ghost bug#24
AboMeezO merged 3 commits into
mainfrom
GhostBug

Conversation

@AboMeezO
Copy link
Copy Markdown
Member

@AboMeezO AboMeezO commented Mar 18, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Improved app stability during startup and initialization with enhanced error handling
    • Fixed single-instance lock behavior to prevent multiple app instances
    • Refined window lifecycle management and tray integration
    • Better error recovery for media file handling
  • Chores

    • Version bumped to 0.35.1

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 18, 2026

📝 Walkthrough

Walkthrough

The Electron main process undergoes significant restructuring with single-instance lock enforcement, early database and IPC initialization, addition of a protocol-based media file handler, reorganized startup sequencing, enhanced window lifecycle management, and unified error handling with improved logging. Version bumped to 0.35.1.

Changes

Cohort / File(s) Summary
Startup Flow & Initialization
electron/main.js
Implements single-instance lock, wraps startup in app.whenReady(), initializes database and IPC handlers early, moves update service initialization into main startup sequence, centralizes error handling with logging before abort, and reorders initialization to database → IPC → update service → window creation.
Protocol & Media Handling
electron/main.js
Adds protocol.handle("media", ...) implementation with URL parsing, path normalization, secure path resolution against allowed bases, MIME type detection, 404/500 error responses, and rejection of non-absolute paths.
Window & Tray Lifecycle
electron/main.js
Reworks window recreation on app.activate, hides to tray on close unless quitting, integrates running project status syncing, refreshes tray state with updated project/category data, adds logging for lifecycle events, and wraps status transmission to renderer with error protection.
Version Update
package.json
Bumps version from 0.35.0 to 0.35.1.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 An electron reborn, with locks held tight,
Media flows through protocol's light,
Database awakens first in the queue,
Then windows dance when startup is through!
Error logs whisper when things go awry,
A sturdy foundation, no need to cry.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Ghost bug' is vague and does not convey meaningful information about the substantial changes made to electron/main.js, including single-instance lock flow, protocol-based media handling, database initialization, and window lifecycle restructuring. Use a more descriptive title that reflects the main change, such as 'Refactor electron app initialization and add media protocol handler' or 'Restructure app startup sequence with single-instance lock and media handling'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch GhostBug
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can make CodeRabbit's review stricter and more nitpicky using the `assertive` profile, if that's what you prefer.

Change the reviews.profile setting to assertive to make CodeRabbit's nitpick more issues in your PRs.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@electron/main.js`:
- Around line 298-303: The code currently validates the lexical path with
resolveAndValidatePath() but then reads the file without resolving symlinks,
allowing a symlink inside an allowed base to point outside; change the flow to
resolve symlinks first (e.g. use fs.realpath or fs.promises.realpath) to obtain
the canonical path, then validate that canonical path against allowedBases with
resolveAndValidatePath() (or add a new validateRealPath function), and only
after validation call readFile() using the canonical path; update filePath to
the resolved real path and keep the same logging (logger.warn) and response
behavior if validation fails.
- Around line 395-406: The callbacks that call refreshTray() (onStatusChange and
onProjectListChange) currently drop its returned promise, risking unhandled
rejections; modify those handlers to await or explicitly handle refreshTray()'s
promise and catch errors (e.g., call refreshTray().catch(err =>
processLogger.error(...)) or use an async wrapper and try/catch) so failures
from refreshTray(), its DB calls or updateTrayMenu() are logged and do not
crash/stop tray updates; ensure you update both onStatusChange and
onProjectListChange handlers and any other locations that call refreshTray()
without handling its promise.
- Around line 268-290: The current fallback adds permissive drive/root bases
when configuredExternalMediaBases is empty, enabling full-drive reads; change
the logic in the block that builds allowedBases (the branch referencing
configuredExternalMediaBases, driveRoot, EXTERNAL_MEDIA_DIRS_ENV_KEY, and
hasLoggedPermissiveExternalMediaMode) to reject external media requests when
SELFHOST_MEDIA_ALLOWED_DIRS is unset: do not push driveRoot or Windows root into
allowedBases, instead leave allowedBases empty (or set a flag to disable
external-media handling) and emit a clear warning/error instructing operators to
set EXTERNAL_MEDIA_DIRS_ENV_KEY; ensure resolveAndValidatePath (the
path-checking code) will then deny all external media access rather than
allowing the permissive root.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: da7813fb-0adc-4f2f-ae6b-f31f92234681

📥 Commits

Reviewing files that changed from the base of the PR and between 327fd0c and 34c4a84.

📒 Files selected for processing (2)
  • electron/main.js
  • package.json

Comment thread electron/main.js
Comment on lines +268 to +290
if (configuredExternalMediaBases.length > 0) {
allowedBases.push(...configuredExternalMediaBases);
} else {
// Backward-compatible permissive mode: allow readable files on the resolved drive root.
const driveRoot = path.parse(path.resolve(filePath)).root;
if (driveRoot) {
allowedBases.push(driveRoot);
}
if (
process.platform === "win32" &&
hostname &&
hostname.length === 1 &&
/^[a-zA-Z]$/.test(hostname)
) {
allowedBases.push(path.resolve(`${hostname}:\\`));
}

if (!hasLoggedPermissiveExternalMediaMode) {
logger.warn(
`[Media Protocol] External media requests are using permissive drive-root mode. Set ${EXTERNAL_MEDIA_DIRS_ENV_KEY} to a ${path.delimiter}-separated list of allowed directories to restrict access.`
);
hasLoggedPermissiveExternalMediaMode = true;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's locate the key files mentioned in the review
git ls-files | grep -E "electron/main.js|ProjectSettingsDialog.jsx"

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 132


🏁 Script executed:

# Check the current state of electron/main.js around lines 268-290
wc -l electron/main.js

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 95


🏁 Script executed:

# Read electron/main.js around lines 268-290 to verify the code snippet
sed -n '260,310p' electron/main.js | cat -n

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 2515


🏁 Script executed:

# Read the protocol handler context - lines 219-336
sed -n '219,336p' electron/main.js | cat -n

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 5867


🏁 Script executed:

# Check for EXTERNAL_MEDIA_DIRS_ENV_KEY or SELFHOST_MEDIA_ALLOWED_DIRS definition
rg "EXTERNAL_MEDIA_DIRS_ENV_KEY|SELFHOST_MEDIA_ALLOWED_DIRS" electron/main.js

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 442


🏁 Script executed:

# Read src/components/ProjectSettingsDialog.jsx around lines 1-50
sed -n '1,50p' src/components/ProjectSettingsDialog.jsx | cat -n

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 1840


🏁 Script executed:

# Search for iconPreview usage in ProjectSettingsDialog.jsx
rg "iconPreview|media://" src/components/ProjectSettingsDialog.jsx -A 3 -B 1

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 1355


🏁 Script executed:

# Find where resolveAndValidatePath is defined and check its implementation
rg "resolveAndValidatePath" electron/main.js -A 10 -B 2

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 1008


🏁 Script executed:

# Search for any symlink-related comments or handling in the protocol handler
rg -i "symlink" electron/main.js

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 57


🏁 Script executed:

# Check the full context to understand configuredExternalMediaBases initialization
sed -n '1,220p' electron/main.js | tail -50 | cat -n

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 1704


🏁 Script executed:

# Find the isPathWithinBase function implementation
rg "isPathWithinBase" electron/main.js -A 5 -B 2

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 1153


🏁 Script executed:

# Get more context about where configuredExternalMediaBases is set
sed -n '1,50p' electron/main.js | cat -n

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 2235


🏁 Script executed:

# Check if formData.icon or icon paths can be set to arbitrary local filesystem paths
rg -i "formData\.icon|icon.*path" src/components/ProjectSettingsDialog.jsx -B 3 -A 3 | head -60

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 1225


🏁 Script executed:

# Check the complete isPathWithinBase implementation
sed -n '44,55p' electron/main.js | cat -n

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 601


🏁 Script executed:

# Check if there's any symlink resolution in the validation
rg "realpath|resolveSymlink" electron/main.js

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 57


🏁 Script executed:

# Verify what happens with fs.promises.access - does it follow symlinks?
# Let's look at how filePath is used after validation
sed -n '85,95p' electron/main.js | cat -n

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 508


🏁 Script executed:

# Check if there are any tests or documentation about the expected behavior
find . -type f \( -name "*.test.js" -o -name "*.spec.js" -o -name "README*" \) | head -10

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 86


🏁 Script executed:

# Let's check if fs.promises.readFile follows symlinks (it should)
# and verify the full file reading flow
sed -n '85,115p' electron/main.js | cat -n

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 1310


🏁 Script executed:

# Check if there are any symlink checks or real path resolution elsewhere
rg "realpath|readlink|symlink" electron/main.js -i

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 57


🏁 Script executed:

# Verify the exact behavior: does the validation happen BEFORE symlink resolution?
# Let's trace the flow from filePath assignment through to file read
sed -n '75,95p' electron/main.js | cat -n

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 765


🏁 Script executed:

# Let's see the complete file reading and media handler code section
sed -n '76,118p' electron/main.js | cat -n

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 1603


🏁 Script executed:

# Check if there's any file picker dialog or input validation for the icon field
rg "ipcMain\|dialog\|file picker" electron/main.js -i -B 2 -A 5 | head -40

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 57


🏁 Script executed:

# Verify: when permissive mode is active, what exactly is allowedBases set to?
# Let's trace through for a Windows example
sed -n '50,73p' electron/main.js | cat -n

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 1047


🏁 Script executed:

# Check the complete media protocol handler - lines 219-310
sed -n '219,310p' electron/main.js | cat -n

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 4728


🏁 Script executed:

# Check if there's any environment variable setup or documentation
cat README.md | head -100

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 6618


🏁 Script executed:

# Verify the exact line numbers for the protocol handler setup
grep -n "protocol.handle" electron/main.js

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 130


🏁 Script executed:

# Let's verify the exact behavior when permissive mode activates
# Specifically, what does driveRoot return on different platforms
python3 << 'EOF'
import os
import sys

# Simulate what Node.js path.parse().root returns
def test_path_root():
    # On POSIX (Linux/Mac), root would be "/"
    # On Windows, root would be "C:\" or similar
    
    # Example paths:
    test_cases = [
        "/etc/passwd",           # POSIX - root is "/"
        "/home/user/file.txt",   # POSIX - root is "/"
        "C:\\Windows\\System32", # Windows - root is "C:\\"
        "D:\\folder\\file.txt",  # Windows - root is "D:\\"
    ]
    
    for path in test_cases:
        if path.startswith("/"):
            root = "/"
        else:
            # Windows path - extract drive letter
            if len(path) > 1 and path[1] == ":":
                root = path[0:2] + "\\"
        print(f"Path: {path} -> Root: {root}")

test_path_root()
EOF

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 217


🏁 Script executed:

# Verify if path.resolve follows symlinks in Node.js behavior
# Check if there are any other safety checks we missed
sed -n '219,310p' electron/main.js | grep -i "symlink\|realpath\|resolve"

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 731


🏁 Script executed:

# Confirm the flow one more time: does user input from formData.icon go directly into the URL?
# Check formData usage in ProjectSettingsDialog
rg "formData\|setFormData" src/components/ProjectSettingsDialog.jsx -A 2 | head -50

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 57


🏁 Script executed:

# Final verification: confirm the exact flow from user input to file read
# Check how icon paths are stored and retrieved from formData
rg "icon.*=" src/components/ProjectSettingsDialog.jsx | head -20

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 139


🏁 Script executed:

# Verify there's no additional validation on the icon path before it's used in media:// URL
sed -n '100,200p' src/components/ProjectSettingsDialog.jsx | grep -A 10 "media://"

Repository: DevRoots-Studio/SelfHost-Helper

Length of output: 57


Reject external media requests when SELFHOST_MEDIA_ALLOWED_DIRS is not configured.

When SELFHOST_MEDIA_ALLOWED_DIRS is unset, the code falls back to permissive mode that allowlists the entire drive on Windows (C:\) and filesystem root on POSIX (/). Combined with the renderer constructing media:// URLs from user-controlled formData.icon in ProjectSettingsDialog.jsx, this becomes a local file disclosure vulnerability. Any absolute path will pass validation since resolveAndValidatePath only checks if the path is within the permitted bases—and when the base is the entire drive root, all files are readable. Since the media:// protocol is registered with supportFetchAPI: true, this exposes a fetch-capable file read primitive.

Suggested fix
             if (configuredExternalMediaBases.length > 0) {
               allowedBases.push(...configuredExternalMediaBases);
             } else {
-              // Backward-compatible permissive mode: allow readable files on the resolved drive root.
-              const driveRoot = path.parse(path.resolve(filePath)).root;
-              if (driveRoot) {
-                allowedBases.push(driveRoot);
-              }
-              if (
-                process.platform === "win32" &&
-                hostname &&
-                hostname.length === 1 &&
-                /^[a-zA-Z]$/.test(hostname)
-              ) {
-                allowedBases.push(path.resolve(`${hostname}:\\`));
-              }
-
-              if (!hasLoggedPermissiveExternalMediaMode) {
-                logger.warn(
-                  `[Media Protocol] External media requests are using permissive drive-root mode. Set ${EXTERNAL_MEDIA_DIRS_ENV_KEY} to a ${path.delimiter}-separated list of allowed directories to restrict access.`
-                );
-                hasLoggedPermissiveExternalMediaMode = true;
-              }
+              logger.warn(
+                `[Media Protocol] Rejecting external media request because ${EXTERNAL_MEDIA_DIRS_ENV_KEY} is not configured.`
+              );
+              return new Response("Forbidden", { status: 403 });
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (configuredExternalMediaBases.length > 0) {
allowedBases.push(...configuredExternalMediaBases);
} else {
// Backward-compatible permissive mode: allow readable files on the resolved drive root.
const driveRoot = path.parse(path.resolve(filePath)).root;
if (driveRoot) {
allowedBases.push(driveRoot);
}
if (
process.platform === "win32" &&
hostname &&
hostname.length === 1 &&
/^[a-zA-Z]$/.test(hostname)
) {
allowedBases.push(path.resolve(`${hostname}:\\`));
}
if (!hasLoggedPermissiveExternalMediaMode) {
logger.warn(
`[Media Protocol] External media requests are using permissive drive-root mode. Set ${EXTERNAL_MEDIA_DIRS_ENV_KEY} to a ${path.delimiter}-separated list of allowed directories to restrict access.`
);
hasLoggedPermissiveExternalMediaMode = true;
}
if (configuredExternalMediaBases.length > 0) {
allowedBases.push(...configuredExternalMediaBases);
} else {
logger.warn(
`[Media Protocol] Rejecting external media request because ${EXTERNAL_MEDIA_DIRS_ENV_KEY} is not configured.`
);
return new Response("Forbidden", { status: 403 });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/main.js` around lines 268 - 290, The current fallback adds
permissive drive/root bases when configuredExternalMediaBases is empty, enabling
full-drive reads; change the logic in the block that builds allowedBases (the
branch referencing configuredExternalMediaBases, driveRoot,
EXTERNAL_MEDIA_DIRS_ENV_KEY, and hasLoggedPermissiveExternalMediaMode) to reject
external media requests when SELFHOST_MEDIA_ALLOWED_DIRS is unset: do not push
driveRoot or Windows root into allowedBases, instead leave allowedBases empty
(or set a flag to disable external-media handling) and emit a clear
warning/error instructing operators to set EXTERNAL_MEDIA_DIRS_ENV_KEY; ensure
resolveAndValidatePath (the path-checking code) will then deny all external
media access rather than allowing the permissive root.

Comment thread electron/main.js
Comment on lines +298 to +303
const resolvedFilePath = resolveAndValidatePath(filePath, allowedBases);
if (!resolvedFilePath) {
logger.warn(`[Media Protocol] Blocked path outside allowed bases: ${filePath}`);
return new Response("Forbidden", { status: 403 });
}
filePath = resolvedFilePath;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Resolve symlinks before enforcing the allowlist.

resolveAndValidatePath() only checks the lexical path. A symlink inside an allowed base still passes this test, and readFile() will then follow it to a target outside the base.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/main.js` around lines 298 - 303, The code currently validates the
lexical path with resolveAndValidatePath() but then reads the file without
resolving symlinks, allowing a symlink inside an allowed base to point outside;
change the flow to resolve symlinks first (e.g. use fs.realpath or
fs.promises.realpath) to obtain the canonical path, then validate that canonical
path against allowedBases with resolveAndValidatePath() (or add a new
validateRealPath function), and only after validation call readFile() using the
canonical path; update filePath to the resolved real path and keep the same
logging (logger.warn) and response behavior if validation fails.

Comment thread electron/main.js
Comment on lines +395 to 406
// Update tray on status changes
onStatusChange(() => {
refreshTray();
});

// Update tray on project list changes (add/delete)
onProjectListChange(() => {
// Update tray on project list changes (add/delete)
onProjectListChange(() => {
refreshTray();
});

// Initial tray setup
refreshTray();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Catch refreshTray() failures.

These callbacks drop the promise returned by refreshTray(). If a DB call or updateTrayMenu() throws, the main process gets an unhandled rejection and tray updates stop degrading cleanly.

Possible fix
       const refreshTray = async () => {
         logger.debug("[Tray] Refreshing menu state.");
         const [projects, categories] = await Promise.all([getProjects(), getCategories()]);
         const runningIds = getRunningProjects();
         updateTrayMenu(
           projects,
           categories,
           runningIds,
           startProject,
           stopProject,
           restartProject,
           startAllProjects,
           stopAllProjects
         );
       };
+      const safeRefreshTray = () => {
+        void refreshTray().catch((error) => {
+          logger.error("[Tray] Failed to refresh menu:", error);
+        });
+      };
 
       tray = initTray(window, () => {
         isQuitting = true;
         app.quit();
       });
 
       // Update tray on status changes
       onStatusChange(() => {
-        refreshTray();
+        safeRefreshTray();
       });
 
       // Update tray on project list changes (add/delete)
       onProjectListChange(() => {
-        refreshTray();
+        safeRefreshTray();
       });
 
       // Initial tray setup
-      refreshTray();
+      safeRefreshTray();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/main.js` around lines 395 - 406, The callbacks that call
refreshTray() (onStatusChange and onProjectListChange) currently drop its
returned promise, risking unhandled rejections; modify those handlers to await
or explicitly handle refreshTray()'s promise and catch errors (e.g., call
refreshTray().catch(err => processLogger.error(...)) or use an async wrapper and
try/catch) so failures from refreshTray(), its DB calls or updateTrayMenu() are
logged and do not crash/stop tray updates; ensure you update both onStatusChange
and onProjectListChange handlers and any other locations that call refreshTray()
without handling its promise.

@AboMeezO AboMeezO merged commit 1d9b418 into main Mar 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant