[PATCH] ima: bound line scan in ima_read_policy() to fix OOB read
Roberto Sassu
roberto.sassu at huaweicloud.com
Wed Aug 26 09:51:48 UTC 2026
On Fri, 2026-08-14 at 10:54 +0200, Fabrice Derepas wrote:
> ima_read_policy() loads a policy file with kernel_read_file_from_path()
> and splits it into lines with
>
> while (size > 0 && (p = strsep(&datap, "\n")))
>
> kernel_read_file() allocates the destination with vmalloc(i_size) --
> exactly i_size bytes, and writes no NUL terminator. strsep()'s scan for
> the next '\n' is not bounded by @size, so when the last line has no
> trailing newline the scan runs off the end of the buffer (CWE-125). When
> i_size is a multiple of PAGE_SIZE the allocation has no slack and the
> read walks into the vmalloc guard page and faults.
>
> Reproduced under KASAN in a VM: writing the path of a page-aligned
> policy file with no trailing newline to <securityfs>/ima/policy oopses:
>
> BUG: unable to handle page fault for address: ffffc90000032000
> #PF: supervisor read access in kernel mode
> RIP: 0010:strsep+0x7a/0xd0
> Call Trace:
> ima_write_policy+0x1f4/0x260
> vfs_write+0x16a/0x6f0
> ksys_write+0xcb/0x160
> do_syscall_64+0xe0/0x5a0
>
> This requires CAP_MAC_ADMIN (the policy file is mode 0200), but a policy
> file that does not end in a newline is an ordinary, non-malicious
> condition, so a legitimate policy load can crash the kernel.
>
> Walk the buffer with memchr() bounded by the remaining size instead of
> strsep(): terminate each line in place at its newline, and parse a
> NUL-terminated copy of a final line that has none. The explicit per-line
> accounting replaces the old "size -= rc" step, whose off-by-one
> (ima_parse_add_rule() returns strlen() + 1) made a trailing line without a
> newline fail with -EINVAL; such a policy now loads. The loop now consumes
> the buffer exactly, so the trailing "if (size) return -EINVAL" is dropped.
Looks unnecessarily complicated. I would replicate instead the same
behavior of ima_write_policy() to allocate a buffer with an additional
byte for the terminator.
Read the inode size, vmalloc() size + 1, set the terminator, and pass
the buffer to kernel_read_file_from_path().
I would still pass NULL for file_size to save ourselves from rechecking
if it changed after the kernel_read_file_from_path() call.
Thanks
Roberto
> Fixes: 7429b092811f ("ima: load policy using path")
> Assisted-by: copilot-cli:claude-opus-4-6 frama-c
> Signed-off-by: Fabrice Derepas <fabrice.derepas at canonical.com>
> ---
> Tested under KASAN (CONFIG_KASAN_GENERIC + CONFIG_KASAN_VMALLOC) in QEMU,
> loading a policy via "echo /path > <securityfs>/ima/policy":
>
> - page-aligned file, no trailing newline: unpatched -> guard-page oops
> in strsep()/ima_read_policy() (trace above); patched -> no fault, the
> load fails cleanly with -EINVAL on the (garbage) content.
> - valid policy with a trailing newline: loads before and after.
> - valid rule with no trailing newline: unpatched -> -EINVAL (the size
> underflow); patched -> loads.
>
> lib/string.o is not KASAN-instrumented, so the over-read is caught by the
> vmalloc guard page rather than a shadow report; the confirmation is the
> page-fault oops with strsep()/ima_write_policy() in the trace.
>
> security/integrity/ima/ima_fs.c | 46 ++++++++++++++++++++++++++-------
> 1 file changed, 36 insertions(+), 10 deletions(-)
>
> diff --git a/security/integrity/ima/ima_fs.c b/security/integrity/ima/ima_fs.c
> index 174a94740..7b530b130 100644
> --- a/security/integrity/ima/ima_fs.c
> +++ b/security/integrity/ima/ima_fs.c
> @@ -526,12 +526,10 @@ static const struct file_operations ima_ascii_measurements_staged_ops = {
> static ssize_t ima_read_policy(char *path)
> {
> void *data = NULL;
> - char *datap;
> - size_t size;
> + char *datap, *eol, *p;
> + size_t size, linelen;
> int rc, pathlen = strlen(path);
>
> - char *p;
> -
> /* remove \n */
> datap = path;
> strsep(&datap, "\n");
> @@ -546,21 +544,49 @@ static ssize_t ima_read_policy(char *path)
> rc = 0;
>
> datap = data;
> - while (size > 0 && (p = strsep(&datap, "\n"))) {
> + while (size > 0) {
> + eol = memchr(datap, '\n', size);
> + linelen = eol ? (size_t)(eol - datap) : size;
> +
> + if (eol) {
> + /* NUL-terminate the line in place, within bounds. */
> + *eol = '\0';
> + p = datap;
> + } else {
> + /*
> + * kernel_read_file_from_path() does not NUL-terminate
> + * the buffer, and it may be exactly i_size bytes long,
> + * so a string walk off the end is possible. The final
> + * line without a trailing newline has no room for a
> + * terminator; parse a terminated copy instead.
> + */
> + p = kmemdup_nul(datap, linelen, GFP_KERNEL);
> + if (!p) {
> + rc = -ENOMEM;
> + break;
> + }
> + }
> +
> pr_debug("rule: %s\n", p);
> rc = ima_parse_add_rule(p);
> + if (!eol)
> + kfree(p);
> if (rc < 0)
> break;
> - size -= rc;
> + rc = 0;
> +
> + datap += linelen;
> + size -= linelen;
> + if (eol) {
> + datap++; /* skip the newline */
> + size--;
> + }
> }
>
> vfree(data);
> if (rc < 0)
> return rc;
> - else if (size)
> - return -EINVAL;
> - else
> - return pathlen;
> + return pathlen;
> }
>
> static ssize_t ima_write_policy(struct file *file, const char __user *buf,
>
> base-commit: d58772d8520c7ef247c4b95c9bd76d3a25da9ff5
More information about the Linux-security-module-archive
mailing list