Re: [email protected], [email protected]
Roberto Sassu <[email protected]> Tue, 28 Jul 2026 10:08:44 +0200
| Newsgroups | org.kernel.vger.linux-integrity |
|---|---|
| Message-ID | <[email protected]> |
On Sat, 2026-07-18 at 08:52 +0200, Fabrice Derepas wrote:
> evm_read_protected_xattrs() accepts a buffer_size parameter but never
> validates it on the 'n' (names) and 'l' (lengths) paths. The sole
> in-tree caller, ima_eventinodexattrs_init_common(), uses a lock-free
> measure-then-allocate-then-fill pattern across two separate calls. A
> concurrent write to /sys/kernel/security/integrity/evm/evm_xattrs
> between the measure and fill calls causes the fill to iterate more
> xattr entries than the buffer was sized for, producing a heap write
> past the end of the allocation.
Hi Fabrice
thanks for reporting the bug.
I need to say, also for everybody, please don't send AI-style bug
reports. I would like to receive a kernel-style commit message
explaining the problem and the solution in just a few sentences. Also,
for the next iteration, please make a real patch.
Thanks
Roberto
> Affected code
> -------------
> security/integrity/evm/evm_main.c, evm_read_protected_xattrs(),
> lines 374-425.
>=20
> The 'n' path writes at line 395 without checking total_size + size
> <=3D buffer_size. The 'l' path stores a u32 at line 404 with the same
> omission. The 'v' path at line 413 correctly passes
> (buffer_size - total_size) to __vfs_getxattr(), so the omission on
> the other two paths is inconsistent with clear design intent:
>=20
> switch (type) {
> case 'n':
> size =3D strlen(xattr->name) + 1;
> if (buffer) {
> if (total_size)
> *(buffer + total_size - 1) =3D '|';
>=20
> memcpy(buffer + total_size, xattr->name, size); /* line 395 -- no
> bounds check */
> }
> break;
> case 'l':
> size =3D sizeof(u32);
> if (buffer) {
> if (canonical_fmt)
> rc =3D (__force int)cpu_to_le32(rc);
>=20
> *(u32 *)(buffer + total_size) =3D rc; /* line 404 -- no
> bounds check */
> }
> break;
> case 'v':
> size =3D rc;
> if (buffer) {
> rc =3D __vfs_getxattr(dentry,
> d_backing_inode(dentry), xattr->name,
> buffer + total_size,
> buffer_size - total_size); /* line 413 -- has bounds check =
*/
> if (rc < 0)
> return rc;
> }
> break;
>=20
>=20
> Root cause
> ----------
> Two defects combine:
>=20
> (1) Missing bounds check (function-level): the 'n' and 'l' paths write
> into buffer + total_size without verifying total_size + size <=3D
> buffer_size. The buffer_size parameter is accepted but unused on
> these paths.
>=20
> (2) TOCTOU race (caller-level): ima_eventinodexattrs_init_common()
> in ima_template_lib.c:703 calls evm_read_protected_xattrs() twice,
> separated by a kmalloc():
>=20
> line 713: rc =3D evm_read_protected_xattrs(..., NULL, 0, ...);
> line 718: buffer =3D kmalloc(rc, GFP_KERNEL);
> line 722: rc =3D evm_read_protected_xattrs(..., buffer, rc, ...);
>=20
> list_for_each_entry_lockless() provides RCU read-side traversal but
> does not stabilise the list across two separate calls. A concurrent
> list_add_tail_rcu() in evm_write_xattrs() (evm_secfs.c:263) between
> lines 713 and 722 causes the fill call to see more entries than the
> buffer accommodates.
>=20
> Note: xattr_list_mutex (evm_secfs.c:24) is held only by the writer;
> there is no read-side lock that would prevent the list from changing
> between the two calls.
>=20
> Neither defect alone is exploitable: without the race, buffer_size
> equals the measured size; without defect (1), an overflow would be
> caught. Together they produce a controlled heap write past the
> allocation.
>=20
>=20
> Reachability
> ------------
> Write path: evm_write_xattrs() (evm_secfs.c:179) handles writes to
> /sys/kernel/security/integrity/evm/evm_xattrs. It requires
> CAP_SYS_ADMIN and evm_xattrs_locked =3D=3D 0 (checked at line 188).
> This path is only compiled when CONFIG_EVM_ADD_XATTRS=3Dy.
>=20
> Read path: ima_eventinodexattrs_init_common() is called during IMA
> event logging for templates that include xattrnames or xattrlengths
> fields (e.g. a custom template with 'n' or 'l' fields). This runs
> on any file access (open, exec, mmap) matched by the active IMA
> policy.
>=20
> Concrete scenario:
>=20
> 1. IMA policy active with a template including xattrnames ('n').
> 2. Initial xattr list: security.selinux (17+1 bytes),
> security.ima (12+1 bytes) -- measure call returns 31.
> 3. kmalloc(31) allocates the buffer.
> 4. CAP_SYS_ADMIN process writes security.apparmor and
> security.capability to evm_xattrs before the fill call.
> 5. Fill call iterates 4 entries, needs ~62 bytes -- overflows
> the 31-byte allocation.
>=20
> The attacker controls write timing and can trigger IMA measurements
> on demand (e.g. by opening a file matching the IMA policy), making
> the race window practically reliable.
>=20
>=20
> Reproducer
> ----------
> The following reproduces the race by simulating the list growth
> between the two calls. Tested with gcc -fsanitize=3Daddress.
>=20
> /*
> * repro_evm_oob.c
> * Compile: gcc -fsanitize=3Daddress -g -o repro_evm_oob repro_evm_oob.=
c
> * Run: ./repro_evm_oob
> */
> #include <stdint.h>
> #include <stdlib.h>
> #include <string.h>
> #include <stdio.h>
>=20
> typedef uint32_t u32;
>=20
> struct xattr_entry {
> const char *name;
> int xattr_size;
> struct xattr_entry *next;
> };
>=20
> static struct xattr_entry *xattr_head =3D NULL;
>=20
> static void add_xattr(const char *name, int sz)
> {
> struct xattr_entry *e =3D malloc(sizeof(*e));
> e->name =3D name; e->xattr_size =3D sz; e->next =3D NULL;
> if (!xattr_head) { xattr_head =3D e; }
> else {
> struct xattr_entry *p =3D xattr_head;
> while (p->next) p =3D p->next;
> p->next =3D e;
> }
> }
>=20
> /* Mirrors evm_main.c:374-425 for type=3D'n'. buffer_size is accepted
> * but never checked -- the bug. */
> static int evm_read_protected_xattrs_sim(uint8_t *buffer,
> int buffer_size, char type)
> {
> struct xattr_entry *xattr;
> int rc, size, total_size =3D 0;
>=20
> for (xattr =3D xattr_head; xattr; xattr =3D xattr->next) {
> rc =3D xattr->xattr_size;
> if (rc < 0) continue;
> switch (type) {
> case 'n':
> size =3D strlen(xattr->name) + 1;
> if (buffer) {
> if (total_size)
> *(buffer + total_size - 1) =3D '|';
> /* BUG: no check that total_size + size <=3D buffer_siz=
e */
> memcpy(buffer + total_size, xattr->name, size);
> }
> break;
> default:
> return -1;
> }
> total_size +=3D size;
> }
> return total_size;
> }
>=20
> int main(void)
> {
> /* Initial list: 2 entries */
> add_xattr("security.selinux", 42);
> add_xattr("security.ima", 32);
>=20
> /* Measuring call (buffer=3DNULL) */
> int measured =3D evm_read_protected_xattrs_sim(NULL, 0, 'n');
> printf("[1] measured=3D%d\n", measured);
>=20
> uint8_t *buf =3D malloc(measured);
> if (!buf) return 1;
>=20
> /* TOCTOU: list grows before fill call */
> add_xattr("security.apparmor", 100);
> add_xattr("security.capability", 20);
>=20
> /* Filling call -- overflows */
> printf("[2] filling...\n");
> int rc =3D evm_read_protected_xattrs_sim(buf, measured, 'n');
> printf("[3] wrote %d into %d-byte buffer\n", rc, measured);
>=20
> free(buf);
> return 0;
> }
>=20
> Observed ASan output:
>=20
> ERROR: AddressSanitizer: heap-buffer-overflow on address 0x...
> WRITE of size 18 at 0x... thread T0
> #0 in memcpy ...
> #1 in evm_read_protected_xattrs_sim repro_evm_oob.c
> #2 in main repro_evm_oob.c
>=20
> 0x... is located 0 bytes after 30-byte region [0x..., 0x...)
> allocated by thread T0 here:
> #0 in malloc ...
> #1 in main repro_evm_oob.c
>=20
>=20
> Impact
> ------
> Heap write past an allocation. The attacker chooses the xattr names
> (write content) and the number of entries added (write size).
> Consequence ranges from a kernel crash (DoS) to, with heap layout
> control, a potential privilege escalation.
>=20
> Requirements: CAP_SYS_ADMIN (to write evm_xattrs), CONFIG_EVM=3Dy,
> CONFIG_IMA=3Dy, CONFIG_EVM_ADD_XATTRS=3Dy, and an active IMA policy
> with a template containing xattrnames or xattrlengths fields.
> CAP_SYS_ADMIN inside a user namespace may suffice on configurations
> that permit it.
>=20
>=20
> Proposed fix
> ------------
> Add bounds checks to the 'n' and 'l' paths, mirroring the existing
> check on the 'v' path:
>=20
> --- a/security/integrity/evm/evm_main.c
> +++ b/security/integrity/evm/evm_main.c
> @@ -388,6 +388,8 @@
> case 'n':
> size =3D strlen(xattr->name) + 1;
> if (buffer) {
> + if (total_size + size > buffer_size)
> + return -ERANGE;
> if (total_size)
> *(buffer + total_size - 1) =3D '|';
>=20
> @@ -398,6 +400,8 @@
> case 'l':
> size =3D sizeof(u32);
> if (buffer) {
> + if (total_size + size > buffer_size)
> + return -ERANGE;
> if (canonical_fmt)
> rc =3D (__force int)cpu_to_le32(rc);
>=20
> A note on the return value: I have used -ERANGE rather than
> returning total_size (partial bytes written). The caller in
> ima_eventinodexattrs_init_common() treats negative returns as an
> error and falls through to kfree(buffer) without logging partial
> data, which is a safer failure mode than silently recording a
> truncated xattr list. The TOCTOU race that triggers the overflow
> is also an integrity anomaly (the xattr list changed under us), so
> treating it as an error rather than silently truncating seems
> correct. Maintainer preference on the return value is welcome.
>=20
> An alternative fix at the caller level would be to hold
> xattr_list_mutex around both calls in
> ima_eventinodexattrs_init_common(), but this requires exposing the
> mutex and the function-level bounds check is more robust against
> future callers.
>=20
>=20
> How this was found
> ------------------
> Found by applying the Squeeze Loop strategy ("The Squeeze Loop
> Strategy: Catching Coherent-and-Wrong Artifacts with an
> Author-Independent Executable Oracle," Zenodo DOI 10.5281/zenodo.20787816=
,
> 2026) on its C terrain - the deductive-verification analogue of
> the paper's Rust application.
>=20
> Signed-off-by: Fabrice Derepas <[email protected]>