diff --git a/meta-python/recipes-devtools/python/python3-filelock/CVE-2026-112936.patch b/meta-python/recipes-devtools/python/python3-filelock/CVE-2026-112936.patch
new file mode 100644
index 0000000000..e4bbc9bd41
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-filelock/CVE-2026-112936.patch
@@ -0,0 +1,382 @@
+From 4d45e83caf7e31f9a12fb44526bdc07f70114f04 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= <gaborjbernat@gmail.com>
+Date: Thu, 1 Oct 2026 13:07:38 -0700
+Subject: [PATCH] Merge commit from fork
+
+ReadWriteLock resolved its path with Path.resolve() and handed it to
+sqlite3.connect(), so a symlink planted in a shared lock directory made
+the victim open, lock, or create the link's target, and repointing the
+link between two constructions let two writers hold the lock at once.
+
+Resolve only the parent directory and open the database through a
+descriptor that refuses a symlink: O_NOFOLLOW and a regular-file check
+on POSIX, with SQLite connecting via /proc/self/fd or /dev/fd, and on
+Windows a handle that refuses reparse points and denies delete sharing.
+Holding that descriptor for the connection's life keeps the name pinned
+to the file that was checked.
+
+GHSA-j8f7-rjxc-mr56
+
+CVE: CVE-2026-112936
+Upstream-Status: Backport [https://github.com/tox-dev/filelock/commit/4d45e83caf7e31f9a12fb44526bdc07f70114f04]
+
+Backport Changes:
+- Adapt parent-only canonicalization and descriptor opening to the
+  3.25.2 constructor without changing public method signatures.
+- Keep compatibility helpers in the new private _rw_security.py.
+- Adapt the later upstream inode-probe pool as a prerequisite for
+  the following shared-path fix and long-lived peer connections.
+- Validate each opened database once and retain its inode identity.
+- Close descriptors after failed fstat; defer unknown-inode closes
+  until all tracked connections close to preserve POSIX locks.
+- Add failed-fstat, non-regular path, symlink and peer-lock cases
+  to a dedicated test_read_write_security.py regression file.
+- Preserve optional SQLite imports in the regression module.
+- Preserve existing Windows opening behavior; omit new handle APIs
+  and newer fork tests because this recipe targets POSIX systems.
+- Omit standalone changelog fragments and newer documentation
+  context not present in the 3.25.2 release source.
+
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ src/filelock/_read_write.py       |   6 +-
+ src/filelock/_rw_security.py      | 171 ++++++++++++++++++++++++++++++
+ tests/test_read_write_security.py | 120 +++++++++++++++++++++
+ 3 files changed, 295 insertions(+), 2 deletions(-)
+ create mode 100644 src/filelock/_rw_security.py
+ create mode 100644 tests/test_read_write_security.py
+
+diff --git a/src/filelock/_read_write.py b/src/filelock/_read_write.py
+index 9e26cbf..e8b6420 100644
+--- a/src/filelock/_read_write.py
++++ b/src/filelock/_read_write.py
+@@ -13,6 +13,7 @@ from weakref import WeakValueDictionary
+ 
+ from ._api import AcquireReturnProxy
+ from ._error import Timeout
++from ._rw_security import canonical, connect
+ 
+ if TYPE_CHECKING:
+     from collections.abc import Generator
+@@ -79,7 +80,7 @@ class _ReadWriteLockMeta(type):
+         if not is_singleton:
+             return super().__call__(lock_file, timeout, blocking=blocking, is_singleton=is_singleton)
+ 
+-        normalized = pathlib.Path(lock_file).resolve()
++        normalized = canonical(lock_file)
+         with cls._instances_lock:
+             if normalized not in cls._instances:
+                 instance = super().__call__(lock_file, timeout, blocking=blocking, is_singleton=is_singleton)
+@@ -154,7 +155,8 @@ class ReadWriteLock(metaclass=_ReadWriteLockMeta):
+         self._lock_level = 0
+         self._current_mode: Literal["read", "write"] | None = None
+         self._write_thread_id: int | None = None
+-        self._con = sqlite3.connect(self.lock_file, check_same_thread=False)
++        self._canonical_path = canonical(lock_file)
++        self._con = connect(os.fspath(self._canonical_path))
+         with _all_connections_lock:
+             _all_connections.add(self._con)
+ 
+diff --git a/src/filelock/_rw_security.py b/src/filelock/_rw_security.py
+new file mode 100644
+index 0000000..6916212
+--- /dev/null
++++ b/src/filelock/_rw_security.py
+@@ -0,0 +1,171 @@
++from __future__ import annotations
++
++import os
++import pathlib
++import sqlite3
++import stat
++import sys
++import threading
++from collections import deque
++from contextlib import suppress
++from dataclasses import dataclass, field
++from itertools import count
++from typing import NamedTuple
++
++_DatabaseIdentity = tuple[int, int]
++_DB_OPEN_FLAGS = os.O_RDWR | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)
++_FD_DIR = "/proc/self/fd" if sys.platform == "linux" else "/dev/fd"
++
++
++def canonical(database: str | os.PathLike[str]) -> pathlib.Path:
++    # Resolve only the parent, so a symlink at the lock path stays its own key instead of aliasing its target.
++    path = pathlib.Path(database)
++    return path.parent.resolve() / path.name
++
++
++class _Probe(NamedTuple):
++    identity: _DatabaseIdentity
++    fd: int
++    # One per connection, so a release applied again after an interrupt kept it queued counts once.
++    lease: int
++
++
++@dataclass
++class _InodeProbes:
++    leases: set[int] = field(default_factory=set)
++    descriptors: list[int] = field(default_factory=list)
++    idle: list[int] = field(default_factory=list)
++
++
++# POSIX locks belong to the process and the inode, so closing any descriptor on the inode drops the locks every other
++# connection of this process holds there. SQLite defers its own closes for this reason (section 2.2 of
++# https://www.sqlite.org/howtocorrupt.html); the probes stay open until the last connection on their inode closes.
++class _ProbeDescriptors:
++    def __init__(self) -> None:
++        self._lock = threading.Lock()
++        self._inodes: dict[_DatabaseIdentity, _InodeProbes] = {}
++        self._unclaimed: list[int] = []
++        self._releases: deque[_Probe] = deque()
++        self._leases = count()
++
++    def adopt(self, database: str, claimed: list[_Probe]) -> _Probe | None:
++        try:
++            status = os.lstat(database)
++        except OSError:
++            return None
++        identity = status.st_dev, status.st_ino
++        with self._lock:
++            probes = self._inodes.get(identity)
++            if not stat.S_ISREG(status.st_mode) or probes is None or not probes.idle:
++                return None
++            claimed.append(adopted := _Probe(identity, probes.idle.pop(), next(self._leases)))
++            probes.leases.add(adopted.lease)
++        return adopted
++
++    def retain(self, fd: int, identity: _DatabaseIdentity, claimed: list[_Probe]) -> _Probe:
++        with self._lock:
++            probes = self._inodes.setdefault(identity, _InodeProbes())
++            probes.descriptors.append(fd)
++            claimed.append(retained := _Probe(identity, fd, next(self._leases)))
++            probes.leases.add(retained.lease)
++        return retained
++
++    def close_unclaimed(self, fd: int) -> None:
++        # If fstat failed, the inode is unknown. Closing it beside a live SQLite connection could release that
++        # connection's POSIX locks, so defer the close until no tracked connection remains.
++        with self._lock:
++            if self._inodes:
++                self._unclaimed.append(fd)
++            else:
++                with suppress(OSError):
++                    os.close(fd)
++
++    def release(self, probe: _Probe) -> None:
++        # A finalizer may run this on a thread already inside the lock, so it never waits; the lock holder drains.
++        self._releases.append(probe)
++        self.drain()
++
++    def drain(self) -> None:
++        while self._releases and self._lock.acquire(blocking=False):
++            try:
++                while self._releases:
++                    self._release_locked(self._releases[0])
++                    self._releases.popleft()
++            finally:
++                self._lock.release()
++
++    def _release_locked(self, probe: _Probe) -> None:
++        if (probes := self._inodes.get(probe.identity)) is None:
++            return
++        if probe.lease in probes.leases:
++            probes.leases.remove(probe.lease)
++            if probes.leases:
++                probes.idle.append(probe.fd)
++        if probes.leases:
++            return
++        probes.idle.clear()
++        # Closing under the lock keeps a concurrent connect from taking a lock on this inode before the close.
++        while probes.descriptors:
++            fd = probes.descriptors.pop()
++            with suppress(OSError):
++                os.close(fd)
++        del self._inodes[probe.identity]
++        if not self._inodes:
++            while self._unclaimed:
++                fd = self._unclaimed.pop()
++                with suppress(OSError):
++                    os.close(fd)
++
++
++_PROBE_DESCRIPTORS = _ProbeDescriptors()
++
++
++class _Connection(sqlite3.Connection):
++    _probe: _Probe | None = None
++    database_identity: _DatabaseIdentity | None = None
++
++    def close(self) -> None:
++        try:
++            super().close()
++        finally:
++            if (probe := self._probe) is not None:
++                self._probe = None
++                _PROBE_DESCRIPTORS.release(probe)
++
++    def __del__(self) -> None:
++        with suppress(Exception):
++            self.close()
++
++
++def _open_lock_database(database: str) -> tuple[int, _DatabaseIdentity]:
++    fd = os.open(database, _DB_OPEN_FLAGS, 0o600)
++    try:
++        status = os.fstat(fd)
++    except BaseException:
++        _PROBE_DESCRIPTORS.close_unclaimed(fd)
++        raise
++    if not stat.S_ISREG(status.st_mode):
++        os.close(fd)
++        msg = f"refusing a non-regular lock database: {database!r}"
++        raise OSError(msg)
++    return fd, (status.st_dev, status.st_ino)
++
++
++def connect(database: str) -> sqlite3.Connection:
++    if sys.platform == "win32":
++        # Windows handle hardening is not part of the POSIX recipe backport.
++        return sqlite3.connect(database, check_same_thread=False)
++    claimed: list[_Probe] = []
++    try:
++        if (probe := _PROBE_DESCRIPTORS.adopt(database, claimed)) is None:
++            fd, identity = _open_lock_database(database)
++            probe = _PROBE_DESCRIPTORS.retain(fd, identity, claimed)
++        _PROBE_DESCRIPTORS.drain()
++        connection = sqlite3.connect(f"{_FD_DIR}/{probe.fd}", check_same_thread=False, factory=_Connection)
++        connection._probe = probe
++        connection.database_identity = probe.identity
++        claimed.clear()
++        return connection
++    finally:
++        for probe in claimed:
++            _PROBE_DESCRIPTORS.release(probe)
+diff --git a/tests/test_read_write_security.py b/tests/test_read_write_security.py
+new file mode 100644
+index 0000000..e2b862a
+--- /dev/null
++++ b/tests/test_read_write_security.py
+@@ -0,0 +1,120 @@
++from __future__ import annotations
++
++import errno
++import os
++import subprocess
++import sys
++from pathlib import Path
++from collections.abc import Generator
++
++import pytest
++
++pytest.importorskip("sqlite3")
++
++import sqlite3
++
++from filelock import ReadWriteLock
++from filelock import _rw_security
++from filelock._read_write import _cleanup_connections
++
++pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="POSIX read-write lock security backport")
++
++
++@pytest.fixture(autouse=True)
++def _close_read_write_connections() -> Generator[None]:
++    _cleanup_connections()
++    ReadWriteLock._instances.clear()
++    yield
++    _cleanup_connections()
++    ReadWriteLock._instances.clear()
++
++
++def test_read_write_lock_refuses_a_symlinked_path(tmp_path: Path) -> None:
++    target = tmp_path / "target.db"
++    target.write_bytes(b"unchanged")
++    database = tmp_path / "lock.db"
++    database.symlink_to(target)
++    with pytest.raises(OSError):
++        ReadWriteLock(database, is_singleton=False)
++    assert target.read_bytes() == b"unchanged"
++
++
++def test_read_write_lock_refuses_a_non_regular_path(tmp_path: Path) -> None:
++    database = tmp_path / "lock.db"
++    os.mkfifo(database)
++    with pytest.raises(OSError, match="non-regular"):
++        ReadWriteLock(database, is_singleton=False)
++
++
++def test_closing_a_peer_keeps_another_connections_lock(tmp_path: Path) -> None:
++    database = tmp_path / "lock.db"
++    holder = ReadWriteLock(database, is_singleton=False)
++    peer = ReadWriteLock(database, is_singleton=False)
++    try:
++        holder.acquire_write()
++        peer.close()
++        result = subprocess.run(
++            [sys.executable, "-c", "import sqlite3,sys; c=sqlite3.connect(sys.argv[1], timeout=0); c.execute('BEGIN EXCLUSIVE')", str(database)],
++            capture_output=True,
++            text=True,
++            check=False,
++        )
++        assert result.returncode != 0
++        assert "database is locked" in result.stderr
++    finally:
++        holder.close()
++
++
++def _fail_opened_database_fstat(database: Path, monkeypatch: pytest.MonkeyPatch) -> list[int]:
++    opened: list[int] = []
++    real_open = os.open
++    real_fstat = os.fstat
++    def open_database(path: str | os.PathLike[str], flags: int, mode: int = 0o777, **kwargs: object) -> int:
++        fd = real_open(path, flags, mode, **kwargs)
++        if os.fspath(path) == os.fspath(database):
++            opened.append(fd)
++        return fd
++    def fail_fstat(fd: int) -> os.stat_result:
++        if fd in opened:
++            raise OSError(errno.EIO, "database fstat failed")
++        return real_fstat(fd)
++    monkeypatch.setattr(os, "open", open_database)
++    monkeypatch.setattr(os, "fstat", fail_fstat)
++    return opened
++
++
++def test_failed_database_fstat_closes_an_unowned_descriptor(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
++    database = tmp_path / "lock.db"
++    with monkeypatch.context() as patch:
++        opened = _fail_opened_database_fstat(database, patch)
++        with pytest.raises(OSError, match="database fstat failed"):
++            ReadWriteLock(database, is_singleton=False)
++    assert len(opened) == 1
++    with pytest.raises(OSError) as caught:
++        os.fstat(opened[0])
++    assert caught.value.errno == errno.EBADF
++
++
++def test_failed_database_fstat_preserves_a_peer_lock(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
++    database = tmp_path / "lock.db"
++    holder = ReadWriteLock(database, is_singleton=False)
++    try:
++        holder.acquire_write()
++        with monkeypatch.context() as patch:
++            opened = _fail_opened_database_fstat(database, patch)
++            with pytest.raises(OSError, match="database fstat failed"):
++                ReadWriteLock(database, is_singleton=False)
++        assert len(opened) == 1
++        # The unidentified descriptor must remain open while closing it could drop the holder's POSIX locks.
++        os.fstat(opened[0])
++        result = subprocess.run(
++            [sys.executable, "-c", "import sqlite3,sys; c=sqlite3.connect(sys.argv[1], timeout=0); c.execute('BEGIN EXCLUSIVE')", str(database)],
++            capture_output=True, text=True, check=False,
++        )
++        assert result.returncode != 0
++        assert "database is locked" in result.stderr
++    finally:
++        holder.close()
++    with pytest.raises(OSError) as caught:
++        os.fstat(opened[0])
++    assert caught.value.errno == errno.EBADF
diff --git a/meta-python/recipes-devtools/python/python3-filelock_3.25.2.bb b/meta-python/recipes-devtools/python/python3-filelock_3.25.2.bb
index a4bd002f1f..db7f9187de 100644
--- a/meta-python/recipes-devtools/python/python3-filelock_3.25.2.bb
+++ b/meta-python/recipes-devtools/python/python3-filelock_3.25.2.bb
@@ -14,6 +14,7 @@ SRC_URI += " file://run-ptest \
     file://CVE-2026-112935_p3.patch \
     file://CVE-2026-112909_p1.patch \
     file://CVE-2026-112909_p2.patch \
+    file://CVE-2026-112936.patch \
 "
 SRC_URI[sha256sum] = "b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694"
 
@@ -33,6 +34,7 @@ RDEPENDS:${PN} += " \
 "
 
 RDEPENDS:${PN}-ptest += " \
+    python3-sqlite3 \
     python3-pytest-asyncio \
     python3-pytest-mock \
     python3-virtualenv \
