Skip to content

fix(install): set DM_DISABLE_UDEV=1 to prevent dm semaphore deadlock in container IPC namespace#2090

Open
andrewdunndev wants to merge 1 commit intobootc-dev:mainfrom
andrewdunndev:fix/luks-udev-cookie-deadlock
Open

fix(install): set DM_DISABLE_UDEV=1 to prevent dm semaphore deadlock in container IPC namespace#2090
andrewdunndev wants to merge 1 commit intobootc-dev:mainfrom
andrewdunndev:fix/luks-udev-cookie-deadlock

Conversation

@andrewdunndev
Copy link
Contributor

@andrewdunndev andrewdunndev commented Mar 24, 2026

Summary

Defense-in-depth fix for the tpm2-luks install hang caused by libdevmapper udev cookie semaphore deadlock in the container's IPC namespace.

The primary fix is to run the install container with --ipc=host. This env var catches cases where IPC sharing is not configured.

Problem

bootc install to-disk --block-setup tpm2-luks hangs indefinitely at cryptsetup luksOpen. The root cause is a SysV semaphore deadlock between libdevmapper and udevd across the container's isolated IPC namespace.

libdevmapper creates SysV semaphores ("udev cookies") to synchronize device-mapper operations with udevd. The container's default IPC namespace is isolated from the host. udevd runs on the host and cannot see the container's semaphore. luksOpen blocks forever on semop().

Kernel stack trace of the hanging process:

[<0>] __do_semtimedop+0x3a8/0xd50
[<0>] do_semtimedop+0x15e/0x1a0
[<0>] do_syscall_64+0x7e/0x6b0

This affects luksOpen and luksClose inside bootc's container environment, regardless of TPM2 configuration.

Fix

Set DM_DISABLE_UDEV=1 in global_init() as defense-in-depth. The primary fix is --ipc=host on the container invocation, which shares the host IPC namespace and allows libdevmapper's udev cookie semaphores to reach udevd. bootc already propagates /run, so IPC sharing is a natural extension.

The env var is placed in global_init() alongside the existing HOME workaround, following the same pattern for process-wide environment setup early in main().

What this changes:

  • dm udev cookie semaphores are not created, so no deadlock even without --ipc=host
  • The kernel still creates /dev/mapper/root via devtmpfs
  • udev rules for dm events don't fire inside the container, but bootc uses /dev/mapper/root directly (never udev symlinks)

What this does NOT change:

  • Partition device nodes and udev_settle() are unaffected
  • The installed system's boot-time behavior is unaffected
  • BlockSetup::Direct path is unaffected

Testing

Three independent tests on Fedora 42 (systemd 257.11, cryptsetup 2.8.4, podman 5.8.0), plus a build verification of the exact patch:

Condition Result
Stock bootc, default podman flags HANG
Stock bootc + DM_DISABLE_UDEV=1 PASS
Stock bootc + --ipc=host PASS
Patched bootc (this PR) PASS

Both workarounds confirm the IPC namespace semaphore as root cause.

Fixes: #2089
Related: #421, #477

@github-actions github-actions bot added the area/install Issues related to `bootc install` label Mar 24, 2026
@bootc-bot bootc-bot bot requested a review from jmarrero March 24, 2026 02:51
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request addresses a deadlock issue with cryptsetup in containerized environments by setting the DM_DISABLE_UDEV=1 environment variable. The fix is well-explained and seems correct. My main feedback is to suggest a safer, unsafe-free way to set the environment variable for the child processes, which would improve the robustness of the implementation.

Comment on lines +165 to +173
#[allow(unsafe_code)]
fn disable_dm_udev_sync() {
// SAFETY: bootc uses a single-threaded tokio runtime (new_current_thread).
// No other threads can race on environment reads. The variable is only
// inherited by child processes (cryptsetup, systemd-cryptenroll).
unsafe {
std::env::set_var("DM_DISABLE_UDEV", "1");
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

While using unsafe to set a process-wide environment variable is justified here with a good safety comment, a safer and more idiomatic approach would be to set the environment variable only for the specific child processes that need it. This avoids unsafe code entirely and more narrowly scopes the change.

You can use std::process::Command::env() to set an environment variable for a specific command. Since the Task helper exposes its Command struct as a public field cmd, you could modify the cryptsetup and systemd-cryptenroll calls to set DM_DISABLE_UDEV on their respective commands.

For example:

let mut task = Task::new("Initializing LUKS for root", "cryptsetup");
task.cmd.env("DM_DISABLE_UDEV", "1");
task.args(["luksFormat", ...]).run()?;

This would allow you to remove the disable_dm_udev_sync function and its unsafe block.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Considered this approach. The reason for the process-wide env var is that luksClose in install.rs (line ~2122, Task::new_and_run("Closing root LUKS device", "cryptsetup", ["close", luksdev])) also needs it, and that call is outside baseline.rs. The per-command approach would require changes in two files and refactoring the new_and_run convenience method to expose cmd.env(). Went with set_var to minimize the blast radius of the PR to one file. Happy to switch to per-command .env() if the maintainer prefers it.

Copy link
Collaborator

@cgwalters cgwalters left a comment

Choose a reason for hiding this comment

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

I reopened and commented on #421 which is about the larger problem here - if we did that then there'd be no devmapper usage at all at installation time by bootc.

The container's default IPC namespace is isolated from the host.

I'd rather change that via changing our default install flow to use --ipc=host. We already propagate /run so this really makes sense.

As far as setenv - we already have global_init() that is the place for this.

…h for dm semaphore deadlock

Move DM_DISABLE_UDEV=1 from a standalone function in baseline.rs to
global_init() in cli.rs, alongside the existing HOME workaround. This
is defense-in-depth for the IPC namespace semaphore deadlock that causes
cryptsetup luksOpen/luksClose to hang inside containers with isolated
IPC namespaces.

The primary fix is to run the install container with --ipc=host, which
shares the host IPC namespace and allows libdevmapper's udev cookie
semaphores to reach udevd. The env var catches cases where IPC sharing
is not configured.

Fixes: bootc-dev#2089
Related: bootc-dev#421, bootc-dev#477

Signed-off-by: Andrew Dunn <andrew@dunn.dev>
AI-Assisted: yes
AI-Tools: GitLab Duo, OpenCode
@andrewdunndev andrewdunndev force-pushed the fix/luks-udev-cookie-deadlock branch from 812ff6a to 23d3fbe Compare March 25, 2026 16:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/install Issues related to `bootc install`

Projects

None yet

Development

Successfully merging this pull request may close these issues.

install to-disk --block-setup tpm2-luks hangs: libdevmapper udev cookie semaphore deadlock in container IPC namespace

2 participants