CVE-2026-90246 (GCVE-0-2026-90246)

Vulnerability from cvelistv5 – Published: 2026-09-17 16:07 – Updated: 2026-09-18 17:54
VLAI
Title
apparmor: fix integer overflow in verify_tags() bounds check
Summary
In the Linux kernel, the following vulnerability has been resolved: apparmor: fix integer overflow in verify_tags() bounds check verify_tags() validates the tagset table unpacked from a policy blob. For each set it reads a count and checks that advancing the index by that count stays inside sets.table[]: u32 cnt = tags->sets.table[i]; if (i+cnt >= tags->sets.size) { i, cnt and sets.size are all u32, so i+cnt is evaluated modulo 2^32. sets.table[] is filled by unpack_tagsets() with aa_unpack_u32(), so every entry is a raw unbounded 32-bit word taken from the policy blob, and verify_tags() is the function that is supposed to validate it. A count close to U32_MAX makes the sum wrap to a small value, the guard passes, and the inner loop then walks sets.table[++i] past the end of the kcalloc(size, sizeof(u32)) allocation. Note that sets.size is bounded by 65535, because unpack_tagsets() reads it with aa_unpack_array() as a u16, so the wrap cannot be reached by growing the table; it is reached purely through the attacker-supplied count. With sets.size = 2 and sets.table = { 0, 0xffffffff }: i = 0: cnt = 0, guard 0 + 0 >= 2 is false, inner loop does not run i = 1: cnt = 0xffffffff, guard (1 + 0xffffffff) mod 2^32 == 0 >= 2 is false, so the guard is bypassed and the inner loop reads sets.table[2] -- one element past a two element allocation The walk continues until an out-of-bounds value happens to be >= hdrs.size or the access faults, so a crafted policy yields an out-of-bounds read on the policy load path (aa_replace_profiles -> aa_unpack -> unpack_policydb -> unpack_tags -> verify_tags). unpack_tags() runs before the perms and DFA tables are unpacked, so no other table needs to be well formed to reach it. Policy load is gated by aa_may_manage_policy(), which checks CAP_MAC_ADMIN relative to the subject's own user namespace rather than the init user namespace, so with the default unprivileged_userns_apparmor_policy=1 the path is reachable from an unprivileged task in a matched-level nested namespace, not only by a globally privileged one. Perform the addition in u64 so that it cannot wrap, restoring the intended i + cnt < sets.size guarantee.
Impacted products
Vendor Product Version CPE status
Linux Linux Affected: 3d28e2397af7a89ac3de33c686ed404cda59b5d5 , < ef79a405f83993f0fda5efde371d98433f7d7a47 (git)
Affected: 3d28e2397af7a89ac3de33c686ed404cda59b5d5 , < 465946d3c560c0137e6a180ba054dddc4d049f5a (git)
guessed Create a notification for this product.
Linux Linux Affected: 7.0
Unaffected: 0 , < 7.0 (semver)
Unaffected: 7.2.6 , ≤ 7.2.* (semver)
Unaffected: 7.3-rc1 , ≤ * (original_commit_for_fix)
guessed Create a notification for this product.
Show details on NVD website

{
  "containers": {
    "cna": {
      "affected": [
        {
          "defaultStatus": "unaffected",
          "product": "Linux",
          "programFiles": [
            "security/apparmor/policy_unpack.c"
          ],
          "repo": "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git",
          "vendor": "Linux",
          "versions": [
            {
              "lessThan": "ef79a405f83993f0fda5efde371d98433f7d7a47",
              "status": "affected",
              "version": "3d28e2397af7a89ac3de33c686ed404cda59b5d5",
              "versionType": "git"
            },
            {
              "lessThan": "465946d3c560c0137e6a180ba054dddc4d049f5a",
              "status": "affected",
              "version": "3d28e2397af7a89ac3de33c686ed404cda59b5d5",
              "versionType": "git"
            }
          ]
        },
        {
          "defaultStatus": "affected",
          "product": "Linux",
          "programFiles": [
            "security/apparmor/policy_unpack.c"
          ],
          "repo": "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git",
          "vendor": "Linux",
          "versions": [
            {
              "status": "affected",
              "version": "7.0"
            },
            {
              "lessThan": "7.0",
              "status": "unaffected",
              "version": "0",
              "versionType": "semver"
            },
            {
              "lessThanOrEqual": "7.2.*",
              "status": "unaffected",
              "version": "7.2.6",
              "versionType": "semver"
            },
            {
              "lessThanOrEqual": "*",
              "status": "unaffected",
              "version": "7.3-rc1",
              "versionType": "original_commit_for_fix"
            }
          ]
        }
      ],
      "cpeApplicability": [
        {
          "nodes": [
            {
              "cpeMatch": [
                {
                  "criteria": "cpe:2.3:o:linux:linux_kernel:*:*:*:*:*:*:*:*",
                  "versionEndExcluding": "7.2.6",
                  "versionStartIncluding": "7.0",
                  "vulnerable": true
                },
                {
                  "criteria": "cpe:2.3:o:linux:linux_kernel:*:*:*:*:*:*:*:*",
                  "versionEndExcluding": "7.3-rc1",
                  "versionStartIncluding": "7.0",
                  "vulnerable": true
                }
              ],
              "negate": false,
              "operator": "OR"
            }
          ]
        }
      ],
      "descriptions": [
        {
          "lang": "en",
          "value": "In the Linux kernel, the following vulnerability has been resolved:\n\napparmor: fix integer overflow in verify_tags() bounds check\n\nverify_tags() validates the tagset table unpacked from a policy blob.\nFor each set it reads a count and checks that advancing the index by\nthat count stays inside sets.table[]:\n\n\tu32 cnt = tags-\u003esets.table[i];\n\n\tif (i+cnt \u003e= tags-\u003esets.size) {\n\ni, cnt and sets.size are all u32, so i+cnt is evaluated modulo 2^32.\nsets.table[] is filled by unpack_tagsets() with aa_unpack_u32(), so\nevery entry is a raw unbounded 32-bit word taken from the policy blob,\nand verify_tags() is the function that is supposed to validate it.  A\ncount close to U32_MAX makes the sum wrap to a small value, the guard\npasses, and the inner loop then walks sets.table[++i] past the end of\nthe kcalloc(size, sizeof(u32)) allocation.\n\nNote that sets.size is bounded by 65535, because unpack_tagsets() reads\nit with aa_unpack_array() as a u16, so the wrap cannot be reached by\ngrowing the table; it is reached purely through the attacker-supplied\ncount.\n\nWith sets.size = 2 and sets.table = { 0, 0xffffffff }:\n\n  i = 0: cnt = 0, guard 0 + 0 \u003e= 2 is false, inner loop does not run\n  i = 1: cnt = 0xffffffff, guard (1 + 0xffffffff) mod 2^32 == 0 \u003e= 2 is\n         false, so the guard is bypassed and the inner loop reads\n         sets.table[2] -- one element past a two element allocation\n\nThe walk continues until an out-of-bounds value happens to be \u003e=\nhdrs.size or the access faults, so a crafted policy yields an\nout-of-bounds read on the policy load path\n(aa_replace_profiles -\u003e aa_unpack -\u003e unpack_policydb -\u003e unpack_tags -\u003e\nverify_tags).  unpack_tags() runs before the perms and DFA tables are\nunpacked, so no other table needs to be well formed to reach it.\n\nPolicy load is gated by aa_may_manage_policy(), which checks\nCAP_MAC_ADMIN relative to the subject\u0027s own user namespace rather than\nthe init user namespace, so with the default\nunprivileged_userns_apparmor_policy=1 the path is reachable from an\nunprivileged task in a matched-level nested namespace, not only by a\nglobally privileged one.\n\nPerform the addition in u64 so that it cannot wrap, restoring the\nintended i + cnt \u003c sets.size guarantee."
        }
      ],
      "metrics": [
        {
          "cvssV3_1": {
            "baseScore": 7.1,
            "baseSeverity": "HIGH",
            "vectorString": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:H",
            "version": "3.1"
          },
          "scenarios": [
            {
              "lang": "en",
              "value": "AV:L - The overflowing tagset count is a u32 in the policy blob the attacker write()s to /sys/kernel/security/apparmor/.load or .replace (profile_load/profile_replace \u2192 policy_update \u2192 aa_replace_profiles \u2192 aa_unpack \u2192 unpack_profile \u2192 unpack_pdb \u2192 unpack_tags \u2192 unpack_tagsets \u2192 verify_tags). The malformed word does not arrive in a network protocol message.\nAC:L - unpack_tagsets() stores attacker-chosen u32s from aa_unpack_u32() into sets.table[]; with sets.size=2 and table {0,0xffffffff}, i+cnt wraps modulo 2^32 to 0 so the bounds guard is skipped every time. No race or victim-side state is required.\nPR:L - policy_update() calls aa_may_manage_policy(), which checks CAP_MAC_ADMIN via policy_ns_capable() against subj_cred-\u003euser_ns, not init_user_ns. With default unprivileged_userns_apparmor_policy=1, aa_policy_view_capable() permits a uid-0 task whose user_ns-\u003elevel equals labels_view(label)-\u003elevel, so unprivileged nested-namespace (container) root can load the crafted blob.\nUI:N - The attacker supplies the packed profile with a tags.sets count of 0xffffffff through their own write to .load/.replace; no other user must mount a volume, open a file, or load policy on their behalf.\nS:U - The OOB read is of the kernel slab object allocated by kcalloc() in unpack_tagsets() while unpacking policy in the host kernel; it does not cross a VM, IOMMU, or other security-authority boundary.\nC:H - After the wrap, verify_tags() executes tags-\u003esets.table[++i] for cnt near U32_MAX, walking u32s past the kcalloc(size,sizeof(u32)) allocation until a value is \u003e= hdrs.size or the access faults. That over-read is not limited to a few bytes.\nI:N - The overflowing loop only reads sets.table[++i] and compares it to hdrs.size; it performs no store through the overflowing index, and aa_unpack() never installs the profile if verify_tags() returns false, oopses, or spins.\nA:H - cnt near U32_MAX causes ~2^32 OOB reads from the sets.table slab object, which oopses when the walk hits unmapped pages; if no OOB word is \u003e= hdrs.size, i wraps and the outer for (i \u003c sets.size) loop in verify_tags() never terminates, hanging the policy-load task."
            }
          ]
        }
      ],
      "providerMetadata": {
        "dateUpdated": "2026-09-18T17:54:09.848Z",
        "orgId": "416baaa9-dc9f-4396-8d5f-8c081fb06d67",
        "shortName": "Linux"
      },
      "references": [
        {
          "url": "https://git.kernel.org/stable/c/ef79a405f83993f0fda5efde371d98433f7d7a47"
        },
        {
          "url": "https://git.kernel.org/stable/c/465946d3c560c0137e6a180ba054dddc4d049f5a"
        }
      ],
      "title": "apparmor: fix integer overflow in verify_tags() bounds check",
      "x_generator": {
        "engine": "bippy-1.2.0"
      }
    }
  },
  "cveMetadata": {
    "assignerOrgId": "416baaa9-dc9f-4396-8d5f-8c081fb06d67",
    "assignerShortName": "Linux",
    "cveId": "CVE-2026-90246",
    "datePublished": "2026-09-17T16:07:48.524Z",
    "dateReserved": "2026-09-11T19:38:34.795Z",
    "dateUpdated": "2026-09-18T17:54:09.848Z",
    "state": "PUBLISHED"
  },
  "dataType": "CVE_RECORD",
  "dataVersion": "5.2",
  "vulnerability-lookup:meta": {
    "epss": {
      "cve": "CVE-2026-90246",
      "date": "2026-09-25",
      "epss": "0.00166",
      "percentile": "0.05167"
    },
    "nvd": {
      "cve": {
        "affected": [
          {
            "affectedData": [
              {
                "defaultStatus": "unaffected",
                "product": "Linux",
                "programFiles": [
                  "security/apparmor/policy_unpack.c"
                ],
                "repo": "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git",
                "vendor": "Linux",
                "versions": [
                  {
                    "lessThan": "ef79a405f83993f0fda5efde371d98433f7d7a47",
                    "status": "affected",
                    "version": "3d28e2397af7a89ac3de33c686ed404cda59b5d5",
                    "versionType": "git"
                  },
                  {
                    "lessThan": "465946d3c560c0137e6a180ba054dddc4d049f5a",
                    "status": "affected",
                    "version": "3d28e2397af7a89ac3de33c686ed404cda59b5d5",
                    "versionType": "git"
                  }
                ]
              },
              {
                "defaultStatus": "affected",
                "product": "Linux",
                "programFiles": [
                  "security/apparmor/policy_unpack.c"
                ],
                "repo": "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git",
                "vendor": "Linux",
                "versions": [
                  {
                    "status": "affected",
                    "version": "7.0"
                  },
                  {
                    "lessThan": "7.0",
                    "status": "unaffected",
                    "version": "0",
                    "versionType": "semver"
                  },
                  {
                    "lessThanOrEqual": "7.2.*",
                    "status": "unaffected",
                    "version": "7.2.6",
                    "versionType": "semver"
                  },
                  {
                    "lessThanOrEqual": "*",
                    "status": "unaffected",
                    "version": "7.3-rc1",
                    "versionType": "original_commit_for_fix"
                  }
                ]
              }
            ],
            "source": "416baaa9-dc9f-4396-8d5f-8c081fb06d67"
          }
        ],
        "cveTags": [],
        "descriptions": [
          {
            "lang": "en",
            "value": "In the Linux kernel, the following vulnerability has been resolved:\n\napparmor: fix integer overflow in verify_tags() bounds check\n\nverify_tags() validates the tagset table unpacked from a policy blob.\nFor each set it reads a count and checks that advancing the index by\nthat count stays inside sets.table[]:\n\n\tu32 cnt = tags-\u003esets.table[i];\n\n\tif (i+cnt \u003e= tags-\u003esets.size) {\n\ni, cnt and sets.size are all u32, so i+cnt is evaluated modulo 2^32.\nsets.table[] is filled by unpack_tagsets() with aa_unpack_u32(), so\nevery entry is a raw unbounded 32-bit word taken from the policy blob,\nand verify_tags() is the function that is supposed to validate it.  A\ncount close to U32_MAX makes the sum wrap to a small value, the guard\npasses, and the inner loop then walks sets.table[++i] past the end of\nthe kcalloc(size, sizeof(u32)) allocation.\n\nNote that sets.size is bounded by 65535, because unpack_tagsets() reads\nit with aa_unpack_array() as a u16, so the wrap cannot be reached by\ngrowing the table; it is reached purely through the attacker-supplied\ncount.\n\nWith sets.size = 2 and sets.table = { 0, 0xffffffff }:\n\n  i = 0: cnt = 0, guard 0 + 0 \u003e= 2 is false, inner loop does not run\n  i = 1: cnt = 0xffffffff, guard (1 + 0xffffffff) mod 2^32 == 0 \u003e= 2 is\n         false, so the guard is bypassed and the inner loop reads\n         sets.table[2] -- one element past a two element allocation\n\nThe walk continues until an out-of-bounds value happens to be \u003e=\nhdrs.size or the access faults, so a crafted policy yields an\nout-of-bounds read on the policy load path\n(aa_replace_profiles -\u003e aa_unpack -\u003e unpack_policydb -\u003e unpack_tags -\u003e\nverify_tags).  unpack_tags() runs before the perms and DFA tables are\nunpacked, so no other table needs to be well formed to reach it.\n\nPolicy load is gated by aa_may_manage_policy(), which checks\nCAP_MAC_ADMIN relative to the subject\u0027s own user namespace rather than\nthe init user namespace, so with the default\nunprivileged_userns_apparmor_policy=1 the path is reachable from an\nunprivileged task in a matched-level nested namespace, not only by a\nglobally privileged one.\n\nPerform the addition in u64 so that it cannot wrap, restoring the\nintended i + cnt \u003c sets.size guarantee."
          }
        ],
        "id": "CVE-2026-90246",
        "lastModified": "2026-09-18T18:17:50.873",
        "metrics": {
          "cvssMetricV31": [
            {
              "cvssData": {
                "attackComplexity": "LOW",
                "attackVector": "LOCAL",
                "availabilityImpact": "HIGH",
                "baseScore": 7.1,
                "baseSeverity": "HIGH",
                "confidentialityImpact": "HIGH",
                "integrityImpact": "NONE",
                "privilegesRequired": "LOW",
                "scope": "UNCHANGED",
                "userInteraction": "NONE",
                "vectorString": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:H",
                "version": "3.1"
              },
              "exploitabilityScore": 1.8,
              "impactScore": 5.2,
              "source": "416baaa9-dc9f-4396-8d5f-8c081fb06d67",
              "type": "Secondary"
            }
          ]
        },
        "published": "2026-09-17T17:17:20.677",
        "references": [
          {
            "source": "416baaa9-dc9f-4396-8d5f-8c081fb06d67",
            "url": "https://git.kernel.org/stable/c/465946d3c560c0137e6a180ba054dddc4d049f5a"
          },
          {
            "source": "416baaa9-dc9f-4396-8d5f-8c081fb06d67",
            "url": "https://git.kernel.org/stable/c/ef79a405f83993f0fda5efde371d98433f7d7a47"
          }
        ],
        "sourceIdentifier": "416baaa9-dc9f-4396-8d5f-8c081fb06d67",
        "vulnStatus": "Received"
      }
    }
  }
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…