diff --git a/dlls/mmdevapi/devenum.c b/dlls/mmdevapi/devenum.c
index b1993573dd9f24b519daa3139507f2fffcdd895e..4733ba6eeeec237b36ab5f22d745ecc6bc222d47 100644
--- a/dlls/mmdevapi/devenum.c
+++ b/dlls/mmdevapi/devenum.c
@@ -47,6 +47,13 @@ DEFINE_GUID(GUID_NULL,0,0,0,0,0,0,0,0,0,0,0);
 static HKEY key_render;
 static HKEY key_capture;
 
+struct clsid_blob {
+    BYTE vt;
+    BYTE wReserved0[3]; // Unknown metadata, maybe flags?
+    DWORD wReserved1; // Possibly a version of some sort?
+    GUID puuid;
+};
+
 typedef struct MMDevPropStoreImpl
 {
     IPropertyStore IPropertyStore_iface;
@@ -196,15 +203,28 @@ static HRESULT MMDevice_GetPropValue(const GUID *devguid, DWORD flow, REFPROPERT
             RegGetValueW(regkey, NULL, buffer, RRF_RT_REG_DWORD, NULL, (BYTE*)&pv->ulVal, &size);
             break;
         }
-        case REG_BINARY:
-        {
-            pv->vt = VT_BLOB;
-            pv->blob.cbSize = size;
-            pv->blob.pBlobData = CoTaskMemAlloc(size);
-            if (!pv->blob.pBlobData)
-                hr = E_OUTOFMEMORY;
-            else
-                RegGetValueW(regkey, NULL, buffer, RRF_RT_REG_BINARY, NULL, (BYTE*)pv->blob.pBlobData, &size);
+        case REG_BINARY: {
+            if (IsEqualPropertyKey(*key, DEVPKEY_Device_ContainerId)) {
+                struct clsid_blob blob;
+                DWORD size = sizeof(blob);
+
+                RegGetValueW(regkey, NULL, buffer, RRF_RT_REG_BINARY, NULL, (BYTE *)&blob, &size);
+
+                pv->vt = VT_CLSID;
+                pv->puuid = CoTaskMemAlloc(sizeof(GUID));
+                if (!pv->puuid)
+                    hr = E_OUTOFMEMORY;
+                else
+                    *pv->puuid = blob.puuid;
+            } else {
+                pv->vt = VT_BLOB;
+                pv->blob.cbSize = size;
+                pv->blob.pBlobData = CoTaskMemAlloc(size);
+                if (!pv->blob.pBlobData)
+                    hr = E_OUTOFMEMORY;
+                else
+                    RegGetValueW(regkey, NULL, buffer, RRF_RT_REG_BINARY, NULL, (BYTE *)pv->blob.pBlobData, &size);
+            }
             break;
         }
         default:
@@ -249,6 +269,11 @@ static HRESULT MMDevice_SetPropValue(const GUID *devguid, DWORD flow, REFPROPERT
             ret = RegSetValueExW(regkey, buffer, 0, REG_SZ, (const BYTE*)pv->pwszVal, sizeof(WCHAR)*(1+lstrlenW(pv->pwszVal)));
             break;
         }
+        case VT_CLSID: {
+            struct clsid_blob blob = {.vt = VT_CLSID, .wReserved0 = {0, 0, 0}, .wReserved1 = 0, .puuid = *pv->puuid};
+            ret = RegSetValueExW(regkey, buffer, 0, REG_BINARY, (const BYTE *)&blob, sizeof(blob));
+            break;
+        }
         default:
             ret = 0;
             FIXME("Unhandled type %u\n", pv->vt);
@@ -416,6 +441,8 @@ static MMDevice *MMDevice_Create(const WCHAR *name, GUID *id, EDataFlow flow, DW
             pv.pwszVal = guidstr;
             MMDevice_SetPropValue(id, flow, &deviceinterface_key, &pv);
 
+            set_driver_prop_value(id, flow, (const PROPERTYKEY*)&DEVPKEY_Device_ContainerId);
+
             if (FAILED(set_driver_prop_value(id, flow, &PKEY_AudioEndpoint_FormFactor)))
             {
                 pv.vt = VT_UI4;
diff --git a/dlls/ntdll/Makefile.in b/dlls/ntdll/Makefile.in
index 37bd6c86e31fb389f3bc53bc56c3bc8aad250d45..323ee4d5c62f28d0b5e72464d308efde5e886561 100644
--- a/dlls/ntdll/Makefile.in
+++ b/dlls/ntdll/Makefile.in
@@ -46,6 +46,7 @@ SOURCES = \
 	threadpool.c \
 	time.c \
 	unix/cdrom.c \
+	unix/containerid.c \
 	unix/debug.c \
 	unix/env.c \
 	unix/file.c \
diff --git a/dlls/ntdll/unix/containerid.c b/dlls/ntdll/unix/containerid.c
new file mode 100644
index 0000000000000000000000000000000000000000..61fa589604918efb6a750929c9f2c911555e1281
--- /dev/null
+++ b/dlls/ntdll/unix/containerid.c
@@ -0,0 +1,196 @@
+/*
+ * ContainerID helper functions
+ *
+ * Copyright 2025 Harald Sitter <sitter@kde.org>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+#if 0
+#pragma makedep unix
+#endif
+
+#include "config.h"
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h> /* Definition of AT_* constants */
+#include <libgen.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "ntstatus.h"
+#define WIN32_NO_STATUS
+#include "windef.h"
+#include "winternl.h"
+#include "wine/containerid.h"
+#include "wine/debug.h"
+
+WINE_DEFAULT_DEBUG_CHANNEL(containerid);
+
+// Find the directory with a 'removeble' file. Mutates sysfs_path in place.
+static BOOL find_removable_file_dir(char *sysfs_path)
+{
+    DIR *device_dir = NULL;
+    struct stat st;
+    for (;;) {
+        if (strcmp("/sys/devices", sysfs_path) == 0) {
+            TRACE("Device is not removable (could not find removable file)\n");
+            return FALSE;
+        }
+        device_dir = opendir(sysfs_path);
+        if (fstatat(dirfd(device_dir), "removable", &st, 0) == 0) {
+            closedir(device_dir);
+            return TRUE;
+        }
+        closedir(device_dir);
+        dirname(sysfs_path); // mutates in place
+    }
+    return FALSE;
+}
+
+// Checks if the device at sysfs_path is removable by checking the contents of the 'removable' file.
+static BOOL is_device_removable(char *sysfs_path)
+{
+    char is_removable_str[MAX_PATH];
+    char removable[] = "removable";
+    DIR *device_dir = opendir(sysfs_path);
+    int fd = openat(dirfd(device_dir), "removable", O_RDONLY | O_CLOEXEC);
+    int err = errno;
+
+    closedir(device_dir);
+
+    if (fd != -1) {
+        read(fd, is_removable_str, sizeof(is_removable_str));
+        close(fd);
+        if (strncmp(is_removable_str, removable, strlen(removable)) == 0) {
+            // Perfect, it's removable, so let's expose the sysfs path and by extension generate a container id.
+            return TRUE;
+        } else {
+            return FALSE;
+            TRACE("Device is not removable, not exposing sysfs path\n");
+        }
+    }
+
+    WARN("Failed to open %s/removable: %s\n", sysfs_path, strerror(err));
+    return FALSE;
+}
+
+static BOOL get_device_sysfs_path_from_sys_path(char const *sysfs_path, char device_path[MAX_PATH])
+{
+    char resolved_sysfs_path[MAX_PATH];
+    // Resolve all parts.
+    if (realpath(sysfs_path, resolved_sysfs_path) == NULL) {
+        WARN("realpath failed: %s\n", strerror(errno));
+        return FALSE;
+    }
+    // Then walk up until we find a removable file marker.
+    if (find_removable_file_dir(resolved_sysfs_path)) {
+        // resolved_sysfs_path is now pointing at the device directory containing a removable file.
+        // Next let's figure out if this device is actually removable.
+        if (is_device_removable(resolved_sysfs_path)) {
+            strcpy(device_path, resolved_sysfs_path);
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
+
+static void container_id_from_inputs(char const **inputs, unsigned inputs_count, GUID *container_id)
+{
+    UINT8 hash[sizeof(GUID)] = {0};
+    UINT8 hash_index = 0;
+
+    // Trivialistic hash function. XOR all the bytes of all the inputs together.
+    for (int i = 0; i < inputs_count; i++) {
+        for (int j = 0; j < strlen(inputs[i]); j++) {
+            hash[hash_index] ^= inputs[i][j];
+            hash_index = (hash_index + 1) % ARRAY_SIZE(hash);
+        }
+    }
+
+    memcpy(container_id, hash, sizeof(GUID));
+}
+
+static  NTSTATUS fill_container_id(char const device_path[MAX_PATH], char const id_product[7], char const id_vendor[7], GUID *container_id)
+{
+    char const *inputs[] = {device_path, id_product, id_vendor};
+
+    // When sysfs_path is empty it means something has gone horribly wrong.
+    if (device_path[0] == 0) {
+        return STATUS_INVALID_PARAMETER;
+    }
+
+    container_id_from_inputs(inputs, ARRAY_SIZE(inputs), container_id);
+    TRACE("Generated container id: %s\n", wine_dbgstr_guid(container_id));
+
+    return STATUS_SUCCESS;
+}
+
+static BOOL read_id_file(char const *sysfs_path, char const *file, char *buffer, size_t buffer_size)
+{
+    DIR *device_dir = opendir(sysfs_path);
+    int fd = openat(dirfd(device_dir), file, O_RDONLY | O_CLOEXEC);
+    int err = errno;
+    off_t offset = 0;
+
+    closedir(device_dir);
+
+    if (fd == -1) {
+        WARN("Failed to open %s/%s: %s\n", sysfs_path, file, strerror(err));
+        return FALSE;
+    }
+
+    for (;;) {
+        ssize_t len = read(fd, buffer + offset, buffer_size - offset);
+        if (len == 0)
+            break;
+        if (len == -1) {
+            if (errno == EINTR)
+                continue;
+            WARN("Failed to read %s/%s: %s\n", sysfs_path, file, strerror(errno));
+            close(fd);
+            return FALSE;
+        }
+    }
+    close(fd);
+    return TRUE;
+
+}
+
+BOOL container_id_for_sysfs(char const *sysfs_path, GUID *container_id)
+{
+    char device_path[MAX_PATH] = {0};
+    char id_product[7] = {0}; // 7 = strlen(0x0b05)+1
+    char id_vendor[7] = {0};
+
+    if (!get_device_sysfs_path_from_sys_path(sysfs_path, device_path)) {
+        return FALSE;
+    }
+
+    if (!read_id_file(device_path, "idProduct", id_product, sizeof(id_product))) {
+        return FALSE;
+    }
+
+    if (!read_id_file(device_path, "idVendor", id_vendor, sizeof(id_vendor))) {
+        return FALSE;
+    }
+
+    fill_container_id(device_path, id_product, id_vendor, container_id);
+    return TRUE;
+}
diff --git a/dlls/winebus.sys/bus_udev.c b/dlls/winebus.sys/bus_udev.c
index 561f0cdc0e4bfe993a54fc79f8bb853ad2cc8f53..f91d998a64a9e912b1aa832784a071d86ca0ebbd 100644
--- a/dlls/winebus.sys/bus_udev.c
+++ b/dlls/winebus.sys/bus_udev.c
@@ -72,6 +72,7 @@
 #include "ddk/hidtypes.h"
 #include "ddk/hidsdi.h"
 
+#include "wine/containerid.h"
 #include "wine/debug.h"
 #include "wine/hid.h"
 #include "wine/unixlib.h"
@@ -1199,6 +1200,30 @@ static void get_device_subsystem_info(struct udev_device *dev, const char *subsy
     }
 }
 
+static const char *get_device_syspath(struct udev_device *dev)
+{
+    struct udev_device *parent;
+
+    if ((parent = udev_device_get_parent_with_subsystem_devtype(dev, "hid", NULL)))
+        return udev_device_get_syspath(parent);
+
+    if ((parent = udev_device_get_parent_with_subsystem_devtype(dev, "usb", "usb_device")))
+        return udev_device_get_syspath(parent);
+
+    return udev_device_get_syspath(dev);
+}
+
+void get_container_id(struct udev_device *dev, struct device_desc *desc)
+{
+    const char *sysfs_path = get_device_syspath(dev);
+
+    memset(&desc->container_id, 0, sizeof(GUID));
+    if (!sysfs_path || sysfs_path[0] == 0) {
+        return;
+    }
+    container_id_for_sysfs(sysfs_path, &desc->container_id);
+}
+
 static void udev_add_device(struct udev_device *dev, int fd)
 {
     struct device_desc desc =
@@ -1224,6 +1249,8 @@ static void udev_add_device(struct udev_device *dev, int fd)
 
     TRACE("udev %s syspath %s\n", debugstr_a(devnode), udev_device_get_syspath(dev));
 
+    get_container_id(dev, &desc);
+
     get_device_subsystem_info(dev, "hid", NULL, &desc, &bus);
     get_device_subsystem_info(dev, "input", NULL, &desc, &bus);
     get_device_subsystem_info(dev, "usb", "usb_device", &desc, &bus);
diff --git a/dlls/winebus.sys/main.c b/dlls/winebus.sys/main.c
index 82bc8667bde87f479a3a72e4df795145c7bd2425..d72fb91bd92a50267dc0b2846a11b051ef9b96c8 100644
--- a/dlls/winebus.sys/main.c
+++ b/dlls/winebus.sys/main.c
@@ -40,6 +40,9 @@
 
 #include "unixlib.h"
 
+#include "initguid.h"
+DEFINE_GUID(GUID_NULL,0,0,0,0,0,0,0,0,0,0,0);
+
 WINE_DEFAULT_DEBUG_CHANNEL(hid);
 
 static DRIVER_OBJECT *driver_obj;
@@ -198,6 +201,21 @@ static WCHAR *get_instance_id(DEVICE_OBJECT *device)
     return dst;
 }
 
+static WCHAR *get_container_id(DEVICE_OBJECT *device)
+{
+    struct device_extension *ext = (struct device_extension *)device->DeviceExtension;
+    UNICODE_STRING dst;
+
+    if (IsEqualGUID(&ext->desc.container_id, &GUID_NULL)) {
+        return NULL;
+    }
+
+    RtlZeroMemory(&dst, sizeof(dst));
+    RtlStringFromGUID(&ext->desc.container_id, &dst);
+
+    return dst.Buffer;
+}
+
 static WCHAR *get_device_id(DEVICE_OBJECT *device)
 {
     static const WCHAR input_format[] = L"&MI_%02u";
@@ -680,6 +698,10 @@ static NTSTATUS handle_IRP_MN_QUERY_ID(DEVICE_OBJECT *device, IRP *irp)
             TRACE("BusQueryInstanceID\n");
             irp->IoStatus.Information = (ULONG_PTR)get_instance_id(device);
             break;
+        case BusQueryContainerID:
+            TRACE("BusQueryContainerID\n");
+            irp->IoStatus.Information = (ULONG_PTR)get_container_id(device);
+            break;
         default:
             WARN("Unhandled type %08x\n", type);
             return status;
diff --git a/dlls/winebus.sys/unixlib.h b/dlls/winebus.sys/unixlib.h
index 02e7a1c6953e588bb5d5bfa7def225cea157fa14..0b898da5e50bdb5d7090ed7db1df518883ec79c9 100644
--- a/dlls/winebus.sys/unixlib.h
+++ b/dlls/winebus.sys/unixlib.h
@@ -45,6 +45,7 @@ struct device_desc
     WCHAR manufacturer[MAX_PATH];
     WCHAR product[MAX_PATH];
     WCHAR serialnumber[MAX_PATH];
+    GUID container_id;
 };
 
 struct sdl_bus_options
diff --git a/dlls/winepulse.drv/pulse.c b/dlls/winepulse.drv/pulse.c
index 050609d2cda96df6cf5ec63d3350637b63618170..8d93efa4b5cd2c07ba1c83aee181e9ef1e202d6b 100644
--- a/dlls/winepulse.drv/pulse.c
+++ b/dlls/winepulse.drv/pulse.c
@@ -38,10 +38,14 @@
 #include "initguid.h"
 #include "audioclient.h"
 
+#include "wine/containerid.h"
 #include "wine/debug.h"
 #include "wine/list.h"
 #include "wine/unixlib.h"
 
+#include "initguid.h"
+#include "devpkey.h"
+
 #include "../mmdevapi/unixlib.h"
 
 #include "mult.h"
@@ -105,6 +109,7 @@ typedef struct _PhysDevice {
     UINT index;
     REFERENCE_TIME min_period, def_period;
     WAVEFORMATEXTENSIBLE fmt;
+    GUID container_id;
     char pulse_name[0];
 } PhysDevice;
 
@@ -549,6 +554,7 @@ static void fill_device_info(PhysDevice *dev, pa_proplist *p)
     dev->bus_type = phys_device_bus_invalid;
     dev->vendor_id = 0;
     dev->product_id = 0;
+    memset(&dev->container_id, 0, sizeof(GUID));
 
     if (!p)
         return;
@@ -565,6 +571,13 @@ static void fill_device_info(PhysDevice *dev, pa_proplist *p)
 
     if ((buffer = pa_proplist_gets(p, PA_PROP_DEVICE_PRODUCT_ID)))
         dev->product_id = strtol(buffer, NULL, 16);
+
+    if ((buffer = pa_proplist_gets(p, "sysfs.path"))) {
+        // The syspath is of the audio device. Resolve it up to the device level.
+        char sysfs_path[MAX_PATH];
+        snprintf(sysfs_path, sizeof(sysfs_path), "/sys%s/device", buffer);
+        container_id_for_sysfs(sysfs_path, &dev->container_id);
+    }
 }
 
 static void pulse_add_device(struct list *list, pa_proplist *proplist, int index, EndpointFormFactor form,
@@ -2621,6 +2634,16 @@ static NTSTATUS pulse_get_prop_value(void *args)
                 params->result = S_OK;
                 return STATUS_SUCCESS;
             }
+        } else if (IsEqualGUID(&params->prop->fmtid, &DEVPKEY_Device_ContainerId)) {
+            params->value->vt = VT_CLSID;
+            params->value->puuid = malloc(sizeof(GUID));
+            if (!params->value->puuid)
+                params->result = E_OUTOFMEMORY;
+            else {
+                params->result = S_OK;
+                *params->value->puuid = dev->container_id;
+            }
+            return STATUS_SUCCESS;
         }
 
         params->result = E_NOTIMPL;
diff --git a/include/Makefile.in b/include/Makefile.in
index cb2b83b6d8cfc48f8d5b456610027644446a0f4e..55732f8b36fea34164aee3dafbca98601277c639 100644
--- a/include/Makefile.in
+++ b/include/Makefile.in
@@ -919,6 +919,7 @@ SOURCES = \
 	wine/afd.h \
 	wine/asm.h \
 	wine/atsvc.idl \
+	wine/containerid.h \
 	wine/condrv.h \
 	wine/dcetypes.idl \
 	wine/debug.h \
diff --git a/include/wine/containerid.h b/include/wine/containerid.h
new file mode 100644
index 0000000000000000000000000000000000000000..0a5b84794ff86794a230ace42e4a21153d70c4e8
--- /dev/null
+++ b/include/wine/containerid.h
@@ -0,0 +1,28 @@
+/*
+ * ContainerID helper functions
+ *
+ * Copyright 2025 Harald Sitter <sitter@kde.org>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+#ifndef _WINE_CONTAINERID
+#define _WINE_CONTAINERID
+
+#include "winternl.h"
+
+NTSYSAPI BOOL container_id_for_sysfs(char const *sysfs_path, GUID *container_id);
+
+#endif /* _WINE_CONTAINERID */