Skip to content

Fix UseSystemd() silently failing with ProtectProc=invisible#125520

Draft
CybCorv wants to merge 8 commits intodotnet:mainfrom
CybCorv:fix/systemd-protect-proc-invisible
Draft

Fix UseSystemd() silently failing with ProtectProc=invisible#125520
CybCorv wants to merge 8 commits intodotnet:mainfrom
CybCorv:fix/systemd-protect-proc-invisible

Conversation

@CybCorv
Copy link
Copy Markdown

@CybCorv CybCorv commented Mar 13, 2026

Summary

UseSystemd() silently fails when the service is configured with
ProtectProc=invisible (recommended by systemd.exec(5) for most services).

GetIsSystemdService() reads /proc/{ppid}/comm to check whether the parent process is
named systemd. Under ProtectProc=invisible, this file is hidden for processes owned by
other users, causing an exception that is silently swallowed — the method returns false,
no IHostLifetime is registered, READY=1 is never sent, and systemd times out waiting for
the service to become ready.

Fix

Use $SYSTEMD_EXEC_PID (introduced in systemd v248) as the primary detection method for
IsSystemdService(). systemd sets this variable to the PID of the main service process;
comparing it to Environment.ProcessId is reliable and unaffected by ProtectProc.

The existing /proc/{ppid}/comm check is kept as a fallback for older systemd versions
still in use on maintained distributions (Ubuntu 20.04 ships systemd 245, Debian 10 ships
systemd 241).

UseSystemd() and AddSystemd() now register the notifier based on NOTIFY_SOCKET alone,
independently of IsSystemdService(). NOTIFY_SOCKET is unset after capture to prevent
child process inheritance. The log formatter is still conditioned by IsSystemdService().

Testing

Tests use RemoteExecutor to isolate the static cache in SystemdHelpers._isSystemdService.

Manual integration test matrix:

Scenario NOTIFY_SOCKET before SYSTEMD_EXEC_PID IsSystemdService NOTIFY_SOCKET after Result
System service, no notify (unset) matching True (unset) ✅ Started
System service + notify present matching True (unset) ✅ Started
System service + notify + ProtectProc=invisible present matching True (unset) ✅ Started
Podman + --sdnotify=container present (unset) True (unset) ✅ Started
Podman + --sdnotify=container + ProtectProc=invisible present (unset) True (unset) ✅ Started
Podman + --sdnotify=container + SYSTEMD_EXEC_PID mismatch present 424242 True (unset) ✅ Started
Docker + notify present (unset) True (unset) ⚠️ timeout*
  • Docker timeout is a pre-existing issue — NOTIFY_SOCKET is not proxied to the notifier socket.

Out of scope

SystemdConsoleFormatter should use $JOURNAL_STREAM + fstat instead of
unconditionally writing journal-formatted output — deferred to a follow-up PR.

GetIsSystemdService() used to read /proc/{ppid}/comm to verify the parent process is named "systemd". This file is hidden when the service runs with ProtectProc=invisible (recommended by systemd.exec(5)), causing the check to silently return false. As a result, no IHostLifetime is registered and READY=1 is never sent, leading to a systemd start timeout.

Fix this by using the SYSTEMD_EXEC_PID environment variable (systemd v248+), which is set to the PID of the main service process. Comparing it to the current PID is reliable and unaffected by ProtectProc=invisible.

The /proc fallback is kept for compatibility with older systemd versions (e.g. Ubuntu 20.04 / systemd 245, Debian 10 / systemd 241).

Partially addresses dotnet#88660
See dotnet#125368
Copilot AI review requested due to automatic review settings March 13, 2026 14:32
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Mar 13, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-extensions-hosting
See info in area-owners.md if you want to be subscribed.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates Microsoft.Extensions.Hosting.Systemd systemd-service detection so UseSystemd() works when /proc access is restricted by ProtectProc=invisible, and adds tests for the new detection behavior.

Changes:

  • Prefer $SYSTEMD_EXEC_PID (systemd v248+) to detect whether the current process is the main systemd service process.
  • Keep the existing /proc/{ppid}/comm check as a legacy fallback for older systemd versions.
  • Add Linux-only RemoteExecutor tests covering $SYSTEMD_EXEC_PID matching/mismatching/missing/malformed and the existing static caching behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs Adds $SYSTEMD_EXEC_PID-based detection before the legacy /proc fallback.
src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs New RemoteExecutor tests for detection behavior and caching.
src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/Microsoft.Extensions.Hosting.Systemd.Tests.csproj Enables RemoteExecutor support for the test project and fixes a trailing space in the ProjectReference.

@CybCorv
Copy link
Copy Markdown
Author

CybCorv commented Mar 13, 2026

Contributor License Agreement

@dotnet-policy-service agree

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 13, 2026 14:45
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes UseSystemd() incorrectly not detecting systemd services when /proc/{ppid}/comm is inaccessible under ProtectProc=invisible, by switching to $SYSTEMD_EXEC_PID (systemd v248+) as the primary detection signal while keeping the /proc parent-name check as a legacy fallback.

Changes:

  • Add $SYSTEMD_EXEC_PID-based detection to SystemdHelpers.GetIsSystemdService() with legacy fallback retained.
  • Add Linux-only RemoteExecutor tests covering env-var detection and caching behavior.
  • Update the test project to include RemoteExecutor infrastructure and fix a malformed ProjectReference.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs Adds RemoteExecutor-isolated tests validating SYSTEMD_EXEC_PID behavior and caching.
src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/Microsoft.Extensions.Hosting.Systemd.Tests.csproj Enables RemoteExecutor support and fixes ProjectReference formatting.
src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs Uses SYSTEMD_EXEC_PID as the primary systemd detection method; retains /proc fallback.

Copilot AI review requested due to automatic review settings March 17, 2026 13:38
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes UseSystemd() incorrectly detecting “not running under systemd” when ProtectProc=invisible prevents reading /proc/<ppid>/comm, by preferring the SYSTEMD_EXEC_PID environment variable (systemd v248+) for detection and keeping the legacy /proc check as a fallback.

Changes:

  • Update systemd-service detection to use SYSTEMD_EXEC_PID (v248+) as the primary signal.
  • Keep the existing /proc/<ppid>/comm parent-name check as a legacy fallback path.
  • Add Linux-only unit tests (using RemoteExecutor) to cover the new detection logic and the static caching behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs Prefer SYSTEMD_EXEC_PID-based detection to avoid /proc access failures under ProtectProc=invisible, with legacy fallback retained.
src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs Adds RemoteExecutor-isolated tests for SYSTEMD_EXEC_PID match/mismatch/absent/malformed and caching behavior.
src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/Microsoft.Extensions.Hosting.Systemd.Tests.csproj Enables RemoteExecutor usage for the new tests and fixes the project reference formatting.

@cincuranet cincuranet requested a review from tmds March 18, 2026 07:37
@cincuranet cincuranet self-assigned this Mar 18, 2026
@cincuranet cincuranet self-requested a review March 18, 2026 07:38
@CybCorv CybCorv marked this pull request as ready for review March 18, 2026 11:11
Copilot AI review requested due to automatic review settings March 18, 2026 11:11
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates Microsoft.Extensions.Hosting.Systemd’s systemd-service detection so UseSystemd() doesn’t silently no-op when /proc access is restricted (e.g., ProtectProc=invisible), and adds focused unit tests for the new detection behavior.

Changes:

  • Prefer $SYSTEMD_EXEC_PID (systemd v248+) to detect running under systemd without relying on /proc.
  • Retain existing /proc/<ppid>/comm parent-name check as a fallback for older systemd versions.
  • Add RemoteExecutor-based tests to validate SYSTEMD_EXEC_PID handling and static caching; enable RemoteExecutor in the test project.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs Adds $SYSTEMD_EXEC_PID-based detection with legacy /proc fallback.
src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs New RemoteExecutor tests covering matching/mismatching/absent/malformed values and caching.
src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/Microsoft.Extensions.Hosting.Systemd.Tests.csproj Enables RemoteExecutor; fixes a stray space in the ProjectReference include.

@CybCorv
Copy link
Copy Markdown
Author

CybCorv commented Mar 18, 2026

Manual integration testing

I've validated the fix against a full test matrix using a dedicated test harness: https://github.com/CybCorv/systemd-dotnet-hosting-tests

Scenario .NET ProtectProc IsSystemdService Result
System service + notify 10 (unfixed) Yes False ❌ timeout
System service + notify 11 (this PR) Yes True ✅ Started
System service + notify 10/11 No True ✅ Started
User service + notify 10/11 Yes* True ✅ Started
Podman + --sdnotify=container 10/11 Yes* True (PID=1) ✅ Started
Docker + notify 10/11 Yes/No True (PID=1) ⚠️ timeout†
  • ProtectProc has no effect in user-scoped services (systemd limitation).
  • Docker timeout is a pre-existing issue -NOTIFY_SOCKET is not proxied to the notifier socket.

@cincuranet
Copy link
Copy Markdown
Contributor

Note

This review was generated with the assistance of AI (GitHub Copilot).

Code Review Summary

Overall this is a well-scoped, correct fix for a real production issue. The integration test matrix is thorough. A couple of points for discussion:

⚠️ SYSTEMD_EXEC_PID mismatch short-circuits fallback detection

When SYSTEMD_EXEC_PID is set and parseable but doesn't match the current PID, return execPid == processId returns false immediately — bypassing the PID 1 + NOTIFY_SOCKET container check and the /proc legacy check.

This could regress containerized scenarios where SYSTEMD_EXEC_PID is inherited from the host (e.g., podman run --env-host or explicit -e SYSTEMD_EXEC_PID=<host-pid>). The manual testing confirms this doesn't happen with default Podman settings, but it's worth considering whether the safer approach is to only return true on match and fall through otherwise:

if (int.TryParse(systemdExecPid, NumberStyles.None, CultureInfo.InvariantCulture, out int execPid))
{
    if (execPid == processId)
    {
        return true;
    }
    // Mismatch: fall through to PID 1 / legacy checks
}

The tradeoff: this would be more defensive but could mask situations where SYSTEMD_EXEC_PID legitimately says "you're not the main service process."

💡 Test gap

Consider adding a test that verifies fallback detection still works when SYSTEMD_EXEC_PID is set but non-matching (e.g., PID 1 + NOTIFY_SOCKET should still succeed if the mismatch fallthrough approach is adopted).

✅ Everything else looks good

  • Core SYSTEMD_EXEC_PID detection logic is correct (NumberStyles.None, CultureInfo.InvariantCulture, malformed fallthrough)
  • Tests are well-structured with proper RemoteExecutor isolation
  • No new public API surface
  • csproj whitespace fix is a nice cleanup

@CybCorv
Copy link
Copy Markdown
Author

CybCorv commented Mar 24, 2026

Thanks for the thorough review @cincuranet .

I agree with the fall-through approach for backwards compatibility. Returning false immediately on mismatch would silently break existing users.

That said, the spec is clear that a mismatch means "you are not the main service process." The fall-through is a compromise imposed by the current design where IsSystemdService() drives both sd_notify and journal formatting (two concerns with different detection criteria). Separating them in follow-up PRs would allow us to restore the strict SYSTEMD_EXEC_PID semantics.

Regarding the suggested test for SYSTEMD_EXEC_PID mismatch + NOTIFY_SOCKET → true: that path requires processId == 1, which isn't injectable in a unit test. I'm adding a manual integration test case to cover it.

I'll update the implementation and the affected test.

I've added a manual integration test for the SYSTEMD_EXEC_PID mismatch + PID=1 + NOTIFY_SOCKET scenario:

Scenario .NET SYSTEMD_EXEC_PID IsSystemdService Result
Podman --sdnotify=container -e SYSTEMD_EXEC_PID=424242 10 424242 (mismatch) True ✅ Started
Podman --sdnotify=container -e SYSTEMD_EXEC_PID=424242 11 (without 9e3f6c7) 424242 (mismatch) False ❌ regression
Podman --sdnotify=container -e SYSTEMD_EXEC_PID=424242 11 (this PR) 424242 (mismatch) True ✅ Started

@tmds
Copy link
Copy Markdown
Member

tmds commented Mar 24, 2026

Out of scope

The discussion in #88660 concluded that what is here as Out of scope to be the proper way to fix the issue.

@CybCorv did you try to implement that? Was there some blocker?

@CybCorv
Copy link
Copy Markdown
Author

CybCorv commented Mar 24, 2026

Out of scope

The discussion in #88660 concluded that what is here as Out of scope to be the proper way to fix the issue.

@CybCorv did you try to implement that? Was there some blocker?

I was aware of the NOTIFY_SOCKET-only approach discussed in #88660. I chose to split the work because SystemdHelpers.IsSystemdService() is a public API with a clear contract: "returns true if the current process is hosted as a systemd service." With ProtectProc=invisible it was returning false for a process that is a systemd service - that's a bug worth fixing on its own.

The broader decoupling (NOTIFY_SOCKET for the notifier, JOURNAL_STREAM for log formatting) likely requires new public API surface, which deserves its own review and discussion.

That said, I'm happy to do it all in one PR if that's the preferred direction. Your call.

@tmds
Copy link
Copy Markdown
Member

tmds commented Mar 24, 2026

The broader decoupling (NOTIFY_SOCKET for the notifier, JOURNAL_STREAM for log formatting) likely requires new public API surface, which deserves its own review and discussion.

Could we extend what is in UseSystemd and AddSystemd so it detects these envvars and acts appropriately without extending the public API?

That said, I'm happy to do it all in one PR if that's the preferred direction. Your call.

If it is something you'd want to work on, I'd prefer you include it in this PR.

UseSystemd() and AddSystemd() now register the notifier based on
NOTIFY_SOCKET alone, and the log formatter based on IsSystemdService().
Unset NOTIFY_SOCKET after capture to prevent child process inheritance.
Centralize environment variable names in SystemdConstants.
Copilot AI review requested due to automatic review settings March 25, 2026 11:05
@CybCorv
Copy link
Copy Markdown
Author

CybCorv commented Mar 25, 2026

If it is something you'd want to work on, I'd prefer you include it in this PR.

I've updated the PR to include the NOTIFY_SOCKET decoupling as discussed.

Manual integration test matrix:

Scenario NOTIFY_SOCKET before SYSTEMD_EXEC_PID IsSystemdService NOTIFY_SOCKET after Result
System service, no notify (unset) matching True (unset) ✅ Started
System service + notify present matching True (unset) ✅ Started
System service + notify + ProtectProc=invisible present matching True (unset) ✅ Started
Podman + --sdnotify=container present (unset) True (unset) ✅ Started
Podman + --sdnotify=container + ProtectProc=invisible present (unset) True (unset) ✅ Started
Podman + --sdnotify=container + SYSTEMD_EXEC_PID mismatch present 424242 True (unset) ✅ Started
Docker + notify present (unset) True (unset) ⚠️ timeout†

Docker timeout is a pre-existing issue. NOTIFY_SOCKET is not proxied to the notifier socket.

Two open questions before marking ready for review:

  1. the new SystemdHelpers.IsSystemdNotify() is currently internal. Given that SystemdHelpers.IsSystemdService() is public, should IsSystemdNotify() also be public for consistency? Or is the preference to keep it internal since the detection logic is an implementation detail of AddSystemd() ?
  2. Should JOURNAL_STREAM + fstat detection for the log formatter be included in this PR, or deferred to a follow-up?

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes UseSystemd()/AddSystemd() not activating correctly when /proc visibility is restricted (e.g., ProtectProc=invisible) by splitting “systemd service detection” from “sd_notify availability” and improving the service detection logic.

Changes:

  • Prefer SYSTEMD_EXEC_PID (systemd v248+) for IsSystemdService() detection, with legacy /proc/{ppid}/comm fallback.
  • Add IsSystemdNotify() detection (based on NOTIFY_SOCKET) and use it to decide whether to register SystemdLifetime/SystemdNotifier.
  • Expand/add tests using RemoteExecutor and enable IncludeRemoteExecutor in the test project.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHelpers.cs Adds SYSTEMD_EXEC_PID-based service detection and introduces cached IsSystemdNotify() detection.
src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdHostBuilderExtensions.cs Registers logger based on IsSystemdService() and lifetime/notifier based on IsSystemdNotify().
src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdNotifier.cs Switches to shared constants and clears NOTIFY_SOCKET after reading it.
src/libraries/Microsoft.Extensions.Hosting.Systemd/src/SystemdConstants.cs Introduces shared env-var constant names and doc links.
src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/UseSystemdTests.cs Adds matrix-style tests for logger vs lifetime registration based on SYSTEMD_EXEC_PID/NOTIFY_SOCKET.
src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/SystemdHelpersTests.cs Adds coverage for SYSTEMD_EXEC_PID parsing/caching behaviors.
src/libraries/Microsoft.Extensions.Hosting.Systemd/tests/Microsoft.Extensions.Hosting.Systemd.Tests.csproj Enables RemoteExecutor and fixes a ProjectReference formatting issue.

Rename test to accurately describe what is verified.
Add namespace to SystemdConstants.
@tmds
Copy link
Copy Markdown
Member

tmds commented Mar 26, 2026

I've updated the PR to include the NOTIFY_SOCKET decoupling as discussed.

👍

I will make some time early next week to review the PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-Extensions-Hosting community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants