Re: [PATCH] usercopy: add testcases to check zeroing on failure of usercopy

From: Hoeun Ryu
Date: Tue Feb 14 2017 - 18:43:15 EST



> On Feb 15, 2017, at 5:36 AM, Kees Cook <keescook@xxxxxxxxxxxx> wrote:
>
>> On Mon, Feb 13, 2017 at 5:44 PM, Hoeun Ryu <hoeun.ryu@xxxxxxxxx> wrote:
>>
>>
>>>> On Feb 14, 2017, at 4:24 AM, Kees Cook <keescook@xxxxxxxxxxxx> wrote:
>>>>
>>>>> On Mon, Feb 13, 2017 at 10:33 AM, Kees Cook <keescook@xxxxxxxxxxxx> wrote:
>>>>> On Sat, Feb 11, 2017 at 10:13 PM, Hoeun Ryu <hoeun.ryu@xxxxxxxxx> wrote:
>>>>> In the hardend usercopy, the destination buffer will be zeroed if
>>>>> copy_from_user/get_user fails. This patch adds testcases for it.
>>>>> The destination buffer is set with non-zero value before illegal
>>>>> copy_from_user/get_user is executed and the buffer is compared to
>>>>> zero after usercopy is done.
>>>>>
>>>>> Signed-off-by: Hoeun Ryu <hoeun.ryu@xxxxxxxxx>
>>>>> ---
>>>>> lib/test_user_copy.c | 17 +++++++++++++++++
>>>>> 1 file changed, 17 insertions(+)
>>>>>
>>>>> diff --git a/lib/test_user_copy.c b/lib/test_user_copy.c
>>>>> index 0ecef3e..54bd898 100644
>>>>> --- a/lib/test_user_copy.c
>>>>> +++ b/lib/test_user_copy.c
>>>>> @@ -41,11 +41,18 @@ static int __init test_user_copy_init(void)
>>>>> char *bad_usermem;
>>>>> unsigned long user_addr;
>>>>> unsigned long value = 0x5A;
>>>>> + char *zerokmem;
>>>>>
>>>>> kmem = kmalloc(PAGE_SIZE * 2, GFP_KERNEL);
>>>>> if (!kmem)
>>>>> return -ENOMEM;
>>>>>
>>>>> + zerokmem = kzalloc(PAGE_SIZE * 2, GFP_KERNEL);
>>>>> + if (!zerokmem) {
>>>>> + kfree(kmem);
>>>>> + return -ENOMEM;
>>>>> + }
>>>>> +
>>>>> user_addr = vm_mmap(NULL, 0, PAGE_SIZE * 2,
>>>>> PROT_READ | PROT_WRITE | PROT_EXEC,
>>>>> MAP_ANONYMOUS | MAP_PRIVATE, 0);
>>>>> @@ -69,25 +76,35 @@ static int __init test_user_copy_init(void)
>>>>> "legitimate put_user failed");
>>>>>
>>>>> /* Invalid usage: none of these should succeed. */
>>>>> + memset(kmem, 0x5A, PAGE_SIZE);
>>>>> ret |= test(!copy_from_user(kmem, (char __user *)(kmem + PAGE_SIZE),
>>>>> PAGE_SIZE),
>>>>> "illegal all-kernel copy_from_user passed");
>>>>> + ret |= test(memcmp(zerokmem, kmem, PAGE_SIZE),
>>>>> + "zeroing failure for illegal all-kernel copy_from_user");
>>>>> + memset(bad_usermem, 0x5A, PAGE_SIZE);
>>>>
>>>> Oh, actually, ha-ha: this isn't legal: it's a direct copy from kernel
>>>> to userspace. :) This needs a copy_to_user()... (and same for the
>>>> memcmp...)
>>
>> I just came up with that usercopy doesn't check the buffer is valid when zeroing happens. I mean if the buffer is wrong address pointing other kernel objects or user space address, is it possible for zeroing to overwrite the address ?
>
> The overwrite happening even when the address is "wrong" seems like a
> bug to me, but it's sort of already too late (a bad kernel address
> would have already been a target for a userspace copy), but if
> something has gone really wrong (i.e. attacker doesn't have control
> over the source buffer) this does give a "write 0" primitive.
>
> Mark Rutland noticed some order-of-operations issues here too, and his
> solution is pretty straight forward: move the checks outside the
> failure path. If the kernel target is demonstrably bad, then the
> process will be killed before the write 0 happens. (In the non-const
> case at least...)
>
> (Oh, btw, I just noticed that x86's copy_from_user() already does the
> check before _copy_from_user() can do the memset, so x86 is already
> "ok" in this regard.)
>
>>>>> + ret |= test(memcmp(zerokmem, kmem, sizeof(value)),
>>>>> + "zeroing failure for illegal get_user");
>>
>> Actually on my x86_64 (qemu), this testcase fails.
>> The generic get_user has zeroing but the one of arch x86 does not.
>> Do we need to propagate zeroing to the other arch specific get_user code ?
>
> Hm, this didn't fail for me on x86 nor arm. Or, at least, my updated
> test doesn't fail:
>
> value = 0x5A;
> ret |= test(!get_user(value, (unsigned long __user *)kmem),
> "illegal get_user passed");
> ret |= test(value != 0, "zeroing failure for illegal get_user");
>
> I see the zeroing in the x86 uaccess.h, though it's a bit obfuscated:
>
> #define get_user(x, ptr) \
> ({ \
> int __ret_gu; \
> register __inttype(*(ptr)) __val_gu asm("%"_ASM_DX); \
> register void *__sp asm(_ASM_SP); \
> __chk_user_ptr(ptr); \
> might_fault(); \
> asm volatile("call __get_user_%P4" \
> : "=a" (__ret_gu), "=r" (__val_gu), "+r" (__sp) \
> : "0" (ptr), "i" (sizeof(*(ptr)))); \
> (x) = (__force __typeof__(*(ptr))) __val_gu; \
> __builtin_expect(__ret_gu, 0); \
> })
>
> __ret_gu is the 0 or -EFAULT (from the __get_user_* assembly), and x
> is set unconditionally to __val_gu, which gets zero-set by the same
> assembly.
>
> Regardless, I'll expand the tests to check all get_user() sizes...
>
> -Kees

Thank you for your detailed explanations.

> --
> Kees Cook
> Pixel Security