diff --git a/meta/classes-recipe/memory-control.bbclass b/meta/classes-recipe/memory-control.bbclass
new file mode 100644
index 0000000000..4b45720a1d
--- /dev/null
+++ b/meta/classes-recipe/memory-control.bbclass
@@ -0,0 +1,95 @@
+# memory-control.bbclass - write a systemd drop-in memory limit into the image
+#
+# Usage:
+#   inherit memory-control
+#   MEMORY_LIMIT = "512M"           # required; K/M/G or raw bytes
+#   MEMORY_OOM_POLICY = "kill"      # optional; "reboot" (default) or "kill"
+#
+# "reboot": system reboots on OOM (vm.panic_on_oom=2; typical platform default).
+# "kill":   only the offending process is killed, system keeps running.
+#           Writes /etc/sysctl.d/memory-control.conf with vm.panic_on_oom=1,
+#           which sorts after vm.conf and overrides it at boot.
+#
+# MemoryHigh is set to 70% of MemoryMax so the kernel has room to reclaim
+# file-backed pages before hitting the hard ceiling.
+#
+# Kernel config prerequisite:
+#   CONFIG_CGROUPS=y and CONFIG_MEMCG=y must be enabled.  A reference kernel
+#   config fragment is installed to ${datadir}/memory-control/cgroups-v2.cfg.
+#   To apply it, copy the fragment to your kernel recipe's files directory and
+#   add it to SRC_URI in your kernel bbappend:
+#
+#     SRC_URI:append:<machine> = " file://cgroups-v2.cfg"
+
+MEMORY_LIMIT ?= ""
+MEMORY_LIMIT_SERVICE ?= "${PN}.service"
+MEMORY_OOM_POLICY ?= "reboot"
+
+RDEPENDS:${PN} += "memory-control"
+
+python memory_control_do_install() {
+    import os
+
+    limit_str = d.getVar('MEMORY_LIMIT') or ''
+    if not limit_str:
+        return
+
+    service    = d.getVar('MEMORY_LIMIT_SERVICE')
+    destdir    = d.getVar('D')
+    oom_policy = (d.getVar('MEMORY_OOM_POLICY') or 'reboot').strip().lower()
+
+    if oom_policy not in ('reboot', 'kill'):
+        bb.fatal("memory-control: MEMORY_OOM_POLICY='%s' is invalid; "
+                 "use 'reboot' or 'kill'" % oom_policy)
+
+    limit_str = limit_str.strip()
+    units = {'K': 1024, 'KB': 1024,
+             'M': 1024**2, 'MB': 1024**2,
+             'G': 1024**3, 'GB': 1024**3}
+    number_part = ''.join(c for c in limit_str if c.isdigit())
+    unit_part   = ''.join(c for c in limit_str if not c.isdigit()).upper()
+
+    if not number_part:
+        bb.fatal("memory-control: MEMORY_LIMIT='%s' is not a valid size" % limit_str)
+
+    bytes_val  = int(number_part) * units.get(unit_part, 1)
+    bytes_high = int(bytes_val * 0.7)
+
+    dropin_dir = os.path.join(destdir, 'etc', 'systemd', 'system', service + '.d')
+    os.makedirs(dropin_dir, exist_ok=True)
+
+    dropin_path = os.path.join(dropin_dir, 'memory-control.conf')
+    with open(dropin_path, 'w') as f:
+        f.write('# generated by memory-control.bbclass\n')
+        f.write('# MEMORY_LIMIT = "%s"  MEMORY_OOM_POLICY = "%s"\n' % (limit_str, oom_policy))
+        f.write('[Service]\n')
+        f.write('MemoryMax=%d\n' % bytes_val)
+        f.write('MemoryHigh=%d\n' % bytes_high)
+        f.write('MemorySwapMax=0\n')
+        if oom_policy == 'kill':
+            # keep the unit alive after the cgroup OOM kill
+            f.write('OOMPolicy=continue\n')
+            # make this unit the preferred victim for the system-wide OOM killer
+            # so it is killed as a process rather than causing a kernel panic
+            f.write('OOMScoreAdjust=500\n')
+
+    bb.note("memory-control: wrote %s for %s (%s, policy=%s)" % (
+            dropin_path, service, limit_str, oom_policy))
+
+    if oom_policy == 'kill':
+        # override vm.panic_on_oom=2 from vm.conf; value 1 kills only the
+        # process inside the cgroup that hit MemoryMax instead of panicking.
+        sysctl_dir  = os.path.join(destdir, 'etc', 'sysctl.d')
+        os.makedirs(sysctl_dir, exist_ok=True)
+        sysctl_path = os.path.join(sysctl_dir, 'memory-control.conf')
+        if not os.path.exists(sysctl_path):
+            with open(sysctl_path, 'w') as f:
+                f.write('# generated by memory-control.bbclass\n')
+                f.write('vm.panic_on_oom=1\n')
+            bb.note("memory-control: wrote %s" % sysctl_path)
+}
+
+do_install[postfuncs] += "memory_control_do_install"
+
+FILES:${PN} += "/etc/systemd/system/${MEMORY_LIMIT_SERVICE}.d/memory-control.conf"
+FILES:${PN} += "${@ '/etc/sysctl.d/memory-control.conf' if (d.getVar('MEMORY_OOM_POLICY') or 'reboot').strip().lower() == 'kill' else '' }"
diff --git a/meta/recipes-support/memory-control/memory-control/cgroups-v2.cfg b/meta/recipes-support/memory-control/memory-control/cgroups-v2.cfg
new file mode 100644
index 0000000000..00661668de
--- /dev/null
+++ b/meta/recipes-support/memory-control/memory-control/cgroups-v2.cfg
@@ -0,0 +1,19 @@
+# kernel config fragment required for cgroup v2 memory accounting
+
+CONFIG_CGROUPS=y
+CONFIG_MEMCG=y
+CONFIG_MEMCG_SWAP=y
+CONFIG_MEMCG_KMEM=y
+CONFIG_CGROUP_SCHED=y
+CONFIG_FAIR_GROUP_SCHED=y
+CONFIG_CFS_BANDWIDTH=y
+CONFIG_CGROUP_PIDS=y
+CONFIG_CGROUP_FREEZER=y
+CONFIG_CGROUP_DEVICE=y
+CONFIG_CGROUP_CPUACCT=y
+CONFIG_CGROUP_PERF=y
+CONFIG_CGROUP_BPF=y
+CONFIG_BLK_CGROUP=y
+CONFIG_PSI=y
+CONFIG_PSI_DEFAULT_DISABLED=n
+CONFIG_SWAP=y
diff --git a/meta/recipes-support/memory-control/memory-control/memory-control.sh b/meta/recipes-support/memory-control/memory-control/memory-control.sh
new file mode 100755
index 0000000000..251c902ca6
--- /dev/null
+++ b/meta/recipes-support/memory-control/memory-control/memory-control.sh
@@ -0,0 +1,196 @@
+#!/bin/bash
+# memory-control - inspect cgroup v2 memory limits for systemd services
+#
+# requires: kernel CONFIG_MEMCG=y + cgroups v2 (mount | grep cgroup2),
+#           systemd 243+, bash 4.0+
+#
+# usage:
+#   memory-control get <service>     show MemoryMax/MemoryHigh
+#   memory-control status <service>  show live usage and memory.stat
+#   memory-control list              list services with a drop-in
+#   .service suffix is optional
+#
+# build-time configuration (recipe or .bbappend):
+#
+#   inherit memory-control
+#   MEMORY_LIMIT      = "512M"       # K/M/G or raw bytes
+#   MEMORY_OOM_POLICY = "kill"       # "reboot" (default) or "kill"
+#
+#   machine overrides work as normal bitbake syntax:
+#     MEMORY_LIMIT:mymachine = "512M"
+#
+#   if the unit name differs from <PN>.service:
+#     MEMORY_LIMIT_SERVICE = "my-custom-unit.service"
+#
+#   accepted sizes: 256K  512M  1G  536870912 (raw bytes)
+#
+# what gets written at build time:
+#
+#   /etc/systemd/system/<service>.service.d/memory-control.conf  (always)
+#     MemoryMax=<bytes>        hard limit; cgroup OOM killer fires if exceeded
+#     MemoryHigh=<70% of max>  reclaim/throttle starts here; 30% gap to max
+#     MemorySwapMax=0          swap disabled
+#     OOMPolicy=continue       kill policy only; unit stays alive after kill
+#     OOMScoreAdjust=500       kill policy only; preferred system-wide OOM target
+#
+#   /etc/sysctl.d/memory-control.conf  (kill policy only)
+#     vm.panic_on_oom=1        sorts after vm.conf, overrides =2 at boot
+#
+# oom policy:
+#   reboot  BMC reboots on OOM; matches vm.panic_on_oom=2 (platform default)
+#   kill    only the offending process dies; BMC keeps running
+#           when MemoryMax is hit:
+#             - cgroup OOM killer fires (scoped to this cgroup only),
+#               vm.panic_on_oom is not consulted
+#             - OOMPolicy=continue keeps the unit alive after the kill
+#             - OOMScoreAdjust=500 makes this the preferred target if global
+#               RAM is also exhausted, avoiding a kernel panic
+#
+# troubleshooting:
+#   cgroups v2 not mounted:
+#     mount | grep cgroup2
+#     # add systemd.unified_cgroup_hierarchy=1 to kernel cmdline if missing
+#
+#   memory controller missing:
+#     cat /sys/fs/cgroup/cgroup.controllers   # "memory" must appear
+#     # rebuild kernel with CONFIG_MEMCG=y if not
+#
+#   drop-in not loaded:
+#     systemctl cat <service>                 # check for memory-control.conf
+#     systemctl daemon-reload && systemctl restart <service>
+#
+#   check kill policy is active:
+#     systemctl show <service> -p OOMPolicy -p MemoryMax -p MemoryHigh
+#     sysctl vm.panic_on_oom                  # should be 1
+#
+#   MemoryMax shows infinity after daemon-reload:
+#     ls /etc/systemd/system/<service>.service.d/
+#     ls /usr/lib/systemd/system/<service>.service.d/
+#     # look for a conflicting drop-in
+#
+#   service keeps getting killed (limit too low):
+#     watch memory-control status <service>   # find peak usage
+#     # set MEMORY_LIMIT to peak + ~30% in the .bbappend and rebuild
+set -e
+
+CGROUP_BASE=/sys/fs/cgroup
+
+usage() {
+    cat <<EOF
+usage: $(basename "$0") <command> [service]
+
+  get <service>    show MemoryMax/MemoryHigh
+  status <service> show live usage and memory.stat
+  list             list services with a memory-control drop-in
+  -h, --help       show this text
+
+Limits are set at build time:
+  inherit memory-control
+  MEMORY_LIMIT      = "512M"
+  MEMORY_OOM_POLICY = "kill"
+
+Drop-in location:
+  /etc/systemd/system/<service>.service.d/memory-control.conf
+EOF
+    exit 0
+}
+
+to_human() {
+    local val=$1
+    case "$val" in
+        max|infinity|"") echo "unlimited"; return ;;
+    esac
+    [[ "$val" =~ ^[0-9]+[KMG]$ ]] && { echo "$val"; return; }
+    [[ "$val" =~ ^[0-9]+$ ]] || { echo "unlimited"; return; }
+    if   (( val >= 1024**3 )); then echo "$((val / 1024**3))G"
+    elif (( val >= 1024**2 )); then echo "$((val / 1024**2))M"
+    elif (( val >= 1024    )); then echo "$((val / 1024))K"
+    else echo "${val}B"
+    fi
+}
+
+resolve_service() {
+    local input=$1
+    [[ "$input" =~ \.service$ ]] || input="${input}.service"
+
+    local fragment
+    fragment=$(systemctl show "$input" -p FragmentPath --value 2>/dev/null)
+
+    if [[ -z "$fragment" || ! -f "$fragment" ]]; then
+        for dir in /etc/systemd/system /lib/systemd/system /usr/lib/systemd/system; do
+            [[ -f "$dir/$input" ]] && { fragment="$dir/$input"; break; }
+        done
+    fi
+
+    if [[ -z "$fragment" || ! -f "$fragment" ]]; then
+        echo "error: no unit file found for '$input'" >&2
+        return 1
+    fi
+
+    service=$(basename "$fragment")
+}
+
+cmd_get() {
+    local service=$1
+    [[ -n "$service" ]] || { echo "error: service name required" >&2; exit 1; }
+    resolve_service "$service" || exit 1
+
+    echo "service:    $service"
+    echo "MemoryMax:  $(to_human "$(systemctl show "$service" -p MemoryMax  --value)")"
+    echo "MemoryHigh: $(to_human "$(systemctl show "$service" -p MemoryHigh --value)")"
+}
+
+cmd_status() {
+    local service=$1
+    [[ -n "$service" ]] || { echo "error: service name required" >&2; exit 1; }
+    resolve_service "$service" || exit 1
+
+    local state
+    state=$(systemctl is-active "$service" 2>/dev/null || true)
+    echo "service: $service"
+    echo "state:   $state"
+
+    if [[ "$state" == "active" ]]; then
+        local mem_max mem_high mem_cur
+        mem_max=$(systemctl show "$service"  -p MemoryMax     --value)
+        mem_high=$(systemctl show "$service" -p MemoryHigh    --value)
+        mem_cur=$(systemctl show "$service"  -p MemoryCurrent --value)
+        echo "current: $(to_human "$mem_cur")"
+        echo "high:    $(to_human "$mem_high")"
+        echo "max:     $(to_human "$mem_max")"
+        [[ "$mem_max" =~ ^[0-9]+$ ]] && echo "usage:   $(( mem_cur * 100 / mem_max ))%"
+    fi
+
+    local cgroup_path
+    cgroup_path=$(systemctl show "$service" -p ControlGroup --value)
+    [[ -z "$cgroup_path" ]] && return
+    printf "\ncgroup: %s\n" "$cgroup_path"
+
+    local mem_stat="${CGROUP_BASE}${cgroup_path}/memory.stat"
+    [[ -f "$mem_stat" ]] || return
+    printf "\nmemory.stat:\n"
+    grep -E "^(anon|file|shmem|kernel)" "$mem_stat" | while read -r key val; do
+        printf "  %-20s %s\n" "$key" "$(to_human "$val")"
+    done
+}
+
+cmd_list() {
+    local found=0
+    for dir in /etc/systemd/system/*.service.d; do
+        [[ -d "$dir" && -f "$dir/memory-control.conf" ]] || continue
+        local unit
+        unit=$(basename "$dir" .d)
+        printf "%-40s %s\n" "${unit%.service}" \
+            "$(to_human "$(systemctl show "$unit" -p MemoryMax --value 2>/dev/null || echo unknown)")"
+        found=1
+    done
+    (( found )) || echo "no services with memory-control drop-ins found"
+}
+
+case "${1:-}" in
+    get)       cmd_get    "$2" ;;
+    status)    cmd_status "$2" ;;
+    list)      cmd_list ;;
+    -h|--help) usage ;;
+    *)         usage ;;
+esac
diff --git a/meta/recipes-support/memory-control/memory-control/memory-control@.service b/meta/recipes-support/memory-control/memory-control/memory-control@.service
new file mode 100644
index 0000000000..20efea5a9c
--- /dev/null
+++ b/meta/recipes-support/memory-control/memory-control/memory-control@.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=Memory Control for %i
+Documentation=man:systemd.resource-control(5)
+Before=%i.service
+PartOf=%i.service
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=/bin/true
+
+[Install]
+WantedBy=multi-user.target
diff --git a/meta/recipes-support/memory-control/memory-control_1.0.bb b/meta/recipes-support/memory-control/memory-control_1.0.bb
new file mode 100644
index 0000000000..3740b783b6
--- /dev/null
+++ b/meta/recipes-support/memory-control/memory-control_1.0.bb
@@ -0,0 +1,53 @@
+SUMMARY = "Inspect and apply cgroup v2 memory limits for systemd services"
+DESCRIPTION = "Provides a CLI tool to inspect MemoryMax/MemoryHigh settings \
+for any systemd service, and a bbclass to bake per-service drop-ins into an \
+image at build time."
+HOMEPAGE = "https://www.openembedded.org"
+BUGTRACKER = "https://bugzilla.yoctoproject.org"
+LICENSE = "MIT"
+LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
+
+inherit systemd
+
+SRC_URI = " \
+    file://memory-control.sh \
+    file://memory-control@.service \
+    file://cgroups-v2.cfg \
+"
+
+SYSTEMD_SERVICE:${PN} = ""
+SYSTEMD_AUTO_ENABLE = "disable"
+
+RDEPENDS:${PN} = "bash systemd"
+
+do_install() {
+    install -d ${D}${bindir}
+    install -m 0755 ${UNPACKDIR}/memory-control.sh ${D}${bindir}/memory-control
+
+    install -d ${D}${systemd_system_unitdir}
+    install -m 0644 ${UNPACKDIR}/memory-control@.service \
+                    ${D}${systemd_system_unitdir}/
+
+    install -d ${D}${datadir}/memory-control
+    install -m 0644 ${UNPACKDIR}/cgroups-v2.cfg \
+                    ${D}${datadir}/memory-control/
+}
+
+pkg_postinst:${PN}() {
+    #!/bin/sh
+    if [ -n "$D" ]; then
+        exit 0
+    fi
+    if [ -f /sys/fs/cgroup/cgroup.controllers ]; then
+        grep -q memory /sys/fs/cgroup/cgroup.controllers || \
+            echo "memory-control: WARNING: memory controller not available in cgroups v2"
+    else
+        echo "memory-control: WARNING: cgroups v2 not detected"
+    fi
+}
+
+FILES:${PN} = " \
+    ${bindir}/memory-control \
+    ${systemd_system_unitdir}/memory-control@.service \
+    ${datadir}/memory-control \
+"
