Message ID | 20200507010504.26352-2-luke.r.nels@gmail.com (mailing list archive) |
---|---|
State | New, archived |
Headers | show |
Series | arm64 BPF JIT Optimizations | expand |
Hi Luke, Thanks a lot for nailing these bugs. On Wed, 6 May 2020 18:05:01 -0700 Luke Nelson <lukenels@cs.washington.edu> wrote: > This patch fixes two issues present in the current function for encoding > arm64 logical immediates when using the 32-bit variants of instructions. > > First, the code does not correctly reject an all-ones 32-bit immediate > and returns an undefined instruction encoding, which can crash the kernel. You make it sound more dramatic than it needs to be! ;-) As you pointed out below, nothing in the kernel calls this code to encode a 32bit immediate, so triggering a crash is not possible (unless you manage to exploit something else to call into this code). It definitely needs fixing though! > The fix is to add a check for this case. > > Second, the code incorrectly rejects some 32-bit immediates that are > actually encodable as logical immediates. The root cause is that the code > uses a default mask of 64-bit all-ones, even for 32-bit immediates. This > causes an issue later on when the mask is used to fill the top bits of > the immediate with ones, shown here: > > /* > * Pattern: 0..01..10..01..1 > * > * Fill the unused top bits with ones, and check if > * the result is a valid immediate (all ones with a > * contiguous ranges of zeroes). > */ > imm |= ~mask; > if (!range_of_ones(~imm)) > return AARCH64_BREAK_FAULT; > > To see the problem, consider an immediate of the form 0..01..10..01..1, > where the upper 32 bits are zero, such as 0x80000001. The code checks > if ~(imm | ~mask) contains a range of ones: the incorrect mask yields > 1..10..01..10..0, which fails the check; the correct mask yields > 0..01..10..0, which succeeds. > > The fix is to use a 32-bit all-ones default mask for 32-bit immediates. Paging this thing back in is really hard (I only had one coffee, more needed). Yes, I see what you mean. Duh! I think this only happens if mask hasn't been adjusted by the "pattern spotting" code the first place though. > > Currently, the only user of this function is in > arch/arm64/kvm/va_layout.c, which uses 64-bit immediates and won't > trigger these bugs. > > We tested the new code against llvm-mc with all 1,302 encodable 32-bit > logical immediates and all 5,334 encodable 64-bit logical immediates. That, on its own, is awesome information. Do you have any pointer on how to set this up? > > Fixes: ef3935eeebff ("arm64: insn: Add encoder for bitwise operations using literals") > Co-developed-by: Xi Wang <xi.wang@gmail.com> > Signed-off-by: Xi Wang <xi.wang@gmail.com> > Signed-off-by: Luke Nelson <luke.r.nels@gmail.com> > --- > arch/arm64/kernel/insn.c | 6 ++++-- > 1 file changed, 4 insertions(+), 2 deletions(-) > > diff --git a/arch/arm64/kernel/insn.c b/arch/arm64/kernel/insn.c > index 4a9e773a177f..42fad79546bb 100644 > --- a/arch/arm64/kernel/insn.c > +++ b/arch/arm64/kernel/insn.c > @@ -1535,7 +1535,7 @@ static u32 aarch64_encode_immediate(u64 imm, > u32 insn) > { > unsigned int immr, imms, n, ones, ror, esz, tmp; > - u64 mask = ~0UL; > + u64 mask; > > /* Can't encode full zeroes or full ones */ > if (!imm || !~imm) > @@ -1543,13 +1543,15 @@ static u32 aarch64_encode_immediate(u64 imm, > > switch (variant) { > case AARCH64_INSN_VARIANT_32BIT: > - if (upper_32_bits(imm)) > + if (upper_32_bits(imm) || imm == 0xffffffffUL) nit: I don't like the fact that this create a small dissymmetry in the way we check things (we start by checking !~imm, which is not relevant to 32bit constants). > return AARCH64_BREAK_FAULT; > esz = 32; > + mask = 0xffffffffUL; > break; > case AARCH64_INSN_VARIANT_64BIT: > insn |= AARCH64_INSN_SF_BIT; > esz = 64; > + mask = ~0UL; I'd rather we generate the mask in a programmatic way, which is pretty easy to do since we have the initial element size. > break; > default: > pr_err("%s: unknown variant encoding %d\n", __func__, variant); To account for the above remarks, I came up with the following patch: diff --git a/arch/arm64/kernel/insn.c b/arch/arm64/kernel/insn.c index 4a9e773a177f..422bf9a79ed6 100644 --- a/arch/arm64/kernel/insn.c +++ b/arch/arm64/kernel/insn.c @@ -1535,11 +1535,7 @@ static u32 aarch64_encode_immediate(u64 imm, u32 insn) { unsigned int immr, imms, n, ones, ror, esz, tmp; - u64 mask = ~0UL; - - /* Can't encode full zeroes or full ones */ - if (!imm || !~imm) - return AARCH64_BREAK_FAULT; + u64 mask; switch (variant) { case AARCH64_INSN_VARIANT_32BIT: @@ -1556,6 +1552,11 @@ static u32 aarch64_encode_immediate(u64 imm, return AARCH64_BREAK_FAULT; } + /* Can't encode full zeroes or full ones */ + mask = GENMASK_ULL(esz - 1, 0); + if (!imm || !(~imm & mask)) + return AARCH64_BREAK_FAULT; + /* * Inverse of Replicate(). Try to spot a repeating pattern * with a pow2 stride. which is of course completely untested (it does compile though). Thoughts? M.
Hi Luke, Thanks for the patches. On Wed, May 06, 2020 at 06:05:01PM -0700, Luke Nelson wrote: > This patch fixes two issues present in the current function for encoding > arm64 logical immediates when using the 32-bit variants of instructions. > > First, the code does not correctly reject an all-ones 32-bit immediate > and returns an undefined instruction encoding, which can crash the kernel. > The fix is to add a check for this case. > > Second, the code incorrectly rejects some 32-bit immediates that are > actually encodable as logical immediates. The root cause is that the code > uses a default mask of 64-bit all-ones, even for 32-bit immediates. This > causes an issue later on when the mask is used to fill the top bits of > the immediate with ones, shown here: > > /* > * Pattern: 0..01..10..01..1 > * > * Fill the unused top bits with ones, and check if > * the result is a valid immediate (all ones with a > * contiguous ranges of zeroes). > */ > imm |= ~mask; > if (!range_of_ones(~imm)) > return AARCH64_BREAK_FAULT; > > To see the problem, consider an immediate of the form 0..01..10..01..1, > where the upper 32 bits are zero, such as 0x80000001. The code checks > if ~(imm | ~mask) contains a range of ones: the incorrect mask yields > 1..10..01..10..0, which fails the check; the correct mask yields > 0..01..10..0, which succeeds. > > The fix is to use a 32-bit all-ones default mask for 32-bit immediates. > > Currently, the only user of this function is in > arch/arm64/kvm/va_layout.c, which uses 64-bit immediates and won't > trigger these bugs. Ah, so this isn't a fix or a bpf patch ;) I can queue it via arm64 for 5.8, along with the bpf patches since there are some other small changes pending in the arm64 bpf backend for BTI. > We tested the new code against llvm-mc with all 1,302 encodable 32-bit > logical immediates and all 5,334 encodable 64-bit logical immediates. > > Fixes: ef3935eeebff ("arm64: insn: Add encoder for bitwise operations using literals") > Co-developed-by: Xi Wang <xi.wang@gmail.com> > Signed-off-by: Xi Wang <xi.wang@gmail.com> > Signed-off-by: Luke Nelson <luke.r.nels@gmail.com> > --- > arch/arm64/kernel/insn.c | 6 ++++-- > 1 file changed, 4 insertions(+), 2 deletions(-) > > diff --git a/arch/arm64/kernel/insn.c b/arch/arm64/kernel/insn.c > index 4a9e773a177f..42fad79546bb 100644 > --- a/arch/arm64/kernel/insn.c > +++ b/arch/arm64/kernel/insn.c > @@ -1535,7 +1535,7 @@ static u32 aarch64_encode_immediate(u64 imm, > u32 insn) > { > unsigned int immr, imms, n, ones, ror, esz, tmp; > - u64 mask = ~0UL; > + u64 mask; > > /* Can't encode full zeroes or full ones */ > if (!imm || !~imm) It's a bit grotty spreading the checks out now. How about we tweak things slightly along the lines of: diff --git a/arch/arm64/kernel/insn.c b/arch/arm64/kernel/insn.c index 4a9e773a177f..60ec788eaf33 100644 --- a/arch/arm64/kernel/insn.c +++ b/arch/arm64/kernel/insn.c @@ -1535,16 +1535,10 @@ static u32 aarch64_encode_immediate(u64 imm, u32 insn) { unsigned int immr, imms, n, ones, ror, esz, tmp; - u64 mask = ~0UL; - - /* Can't encode full zeroes or full ones */ - if (!imm || !~imm) - return AARCH64_BREAK_FAULT; + u64 mask; switch (variant) { case AARCH64_INSN_VARIANT_32BIT: - if (upper_32_bits(imm)) - return AARCH64_BREAK_FAULT; esz = 32; break; case AARCH64_INSN_VARIANT_64BIT: @@ -1556,6 +1550,12 @@ static u32 aarch64_encode_immediate(u64 imm, return AARCH64_BREAK_FAULT; } + mask = GENMASK(esz - 1, 0); + + /* Can't encode full zeroes or full ones */ + if (imm & ~mask || !imm || imm == mask) + return AARCH64_BREAK_FAULT; + /* * Inverse of Replicate(). Try to spot a repeating pattern * with a pow2 stride. What do you think? Will
On Thu, 7 May 2020 09:29:35 +0100 Will Deacon <will@kernel.org> wrote: Hi Will, > Hi Luke, > > Thanks for the patches. > > On Wed, May 06, 2020 at 06:05:01PM -0700, Luke Nelson wrote: > > This patch fixes two issues present in the current function for encoding > > arm64 logical immediates when using the 32-bit variants of instructions. > > > > First, the code does not correctly reject an all-ones 32-bit immediate > > and returns an undefined instruction encoding, which can crash the kernel. > > The fix is to add a check for this case. > > > > Second, the code incorrectly rejects some 32-bit immediates that are > > actually encodable as logical immediates. The root cause is that the code > > uses a default mask of 64-bit all-ones, even for 32-bit immediates. This > > causes an issue later on when the mask is used to fill the top bits of > > the immediate with ones, shown here: > > > > /* > > * Pattern: 0..01..10..01..1 > > * > > * Fill the unused top bits with ones, and check if > > * the result is a valid immediate (all ones with a > > * contiguous ranges of zeroes). > > */ > > imm |= ~mask; > > if (!range_of_ones(~imm)) > > return AARCH64_BREAK_FAULT; > > > > To see the problem, consider an immediate of the form 0..01..10..01..1, > > where the upper 32 bits are zero, such as 0x80000001. The code checks > > if ~(imm | ~mask) contains a range of ones: the incorrect mask yields > > 1..10..01..10..0, which fails the check; the correct mask yields > > 0..01..10..0, which succeeds. > > > > The fix is to use a 32-bit all-ones default mask for 32-bit immediates. > > > > Currently, the only user of this function is in > > arch/arm64/kvm/va_layout.c, which uses 64-bit immediates and won't > > trigger these bugs. > > Ah, so this isn't a fix or a bpf patch ;) > > I can queue it via arm64 for 5.8, along with the bpf patches since there > are some other small changes pending in the arm64 bpf backend for BTI. > > > We tested the new code against llvm-mc with all 1,302 encodable 32-bit > > logical immediates and all 5,334 encodable 64-bit logical immediates. > > > > Fixes: ef3935eeebff ("arm64: insn: Add encoder for bitwise operations using literals") > > Co-developed-by: Xi Wang <xi.wang@gmail.com> > > Signed-off-by: Xi Wang <xi.wang@gmail.com> > > Signed-off-by: Luke Nelson <luke.r.nels@gmail.com> > > --- > > arch/arm64/kernel/insn.c | 6 ++++-- > > 1 file changed, 4 insertions(+), 2 deletions(-) > > > > diff --git a/arch/arm64/kernel/insn.c b/arch/arm64/kernel/insn.c > > index 4a9e773a177f..42fad79546bb 100644 > > --- a/arch/arm64/kernel/insn.c > > +++ b/arch/arm64/kernel/insn.c > > @@ -1535,7 +1535,7 @@ static u32 aarch64_encode_immediate(u64 imm, > > u32 insn) > > { > > unsigned int immr, imms, n, ones, ror, esz, tmp; > > - u64 mask = ~0UL; > > + u64 mask; > > > > /* Can't encode full zeroes or full ones */ > > if (!imm || !~imm) > > It's a bit grotty spreading the checks out now. How about we tweak things > slightly along the lines of: > > > diff --git a/arch/arm64/kernel/insn.c b/arch/arm64/kernel/insn.c > index 4a9e773a177f..60ec788eaf33 100644 > --- a/arch/arm64/kernel/insn.c > +++ b/arch/arm64/kernel/insn.c > @@ -1535,16 +1535,10 @@ static u32 aarch64_encode_immediate(u64 imm, > u32 insn) > { > unsigned int immr, imms, n, ones, ror, esz, tmp; > - u64 mask = ~0UL; > - > - /* Can't encode full zeroes or full ones */ > - if (!imm || !~imm) > - return AARCH64_BREAK_FAULT; > + u64 mask; > > switch (variant) { > case AARCH64_INSN_VARIANT_32BIT: > - if (upper_32_bits(imm)) > - return AARCH64_BREAK_FAULT; > esz = 32; > break; > case AARCH64_INSN_VARIANT_64BIT: > @@ -1556,6 +1550,12 @@ static u32 aarch64_encode_immediate(u64 imm, > return AARCH64_BREAK_FAULT; > } > > + mask = GENMASK(esz - 1, 0); > + > + /* Can't encode full zeroes or full ones */ ... nor a value wider than the mask. > + if (imm & ~mask || !imm || imm == mask) > + return AARCH64_BREAK_FAULT; > + > /* > * Inverse of Replicate(). Try to spot a repeating pattern > * with a pow2 stride. > > > What do you think? I'd be pretty happy with that. Reviewed-by: Marc Zyngier <maz@kernel.org> Thanks, M.
Hi everyone, Thanks for the comments! Responses below: > It's a bit grotty spreading the checks out now. How about we tweak things > slightly along the lines of: > > > diff --git a/arch/arm64/kernel/insn.c b/arch/arm64/kernel/insn.c > index 4a9e773a177f..60ec788eaf33 100644 > --- a/arch/arm64/kernel/insn.c > +++ b/arch/arm64/kernel/insn.c > [...] Agreed; this new version looks much cleaner. I re-ran all the tests / verification and everything seems good. Would you like me to submit a v2 of this series with this new code? >> We tested the new code against llvm-mc with all 1,302 encodable 32-bit >> logical immediates and all 5,334 encodable 64-bit logical immediates. > > That, on its own, is awesome information. Do you have any pointer on > how to set this up? Sure! The process of running the tests is pretty involved, but I'll describe it below and give some links here. We found the bugs in insn.c while adding support for logical immediates to the BPF JIT and verifying the changes with our tool, Serval: https://github.com/uw-unsat/serval-bpf. The basic idea for how we tested / verified logical immediates is the following: First, we have a Python script [1] for generating every encodable logical immediate and the corresponding instruction fields that encode that immediate. The script validates the list by checking that llvm-mc decodes each instruction back to the expected immediate. Next, we use the list [2] from the first step to check a Racket translation [3] of the logical immediate encoding function in insn.c. We found the second mask bug by noticing that some (encodable) 32-bit immediates were being rejected by the encoding function. Last, we use the Racket translation of the encoding function to verify the correctness of the BPF JIT implementation [4], i.e., the JIT correctly compiles BPF_{AND,OR,XOR,JSET} BPF_K instructions to arm64 instructions with equivalent semantics. We found the first bug as the verifier complained that the function was producing invalid encodings for 32-bit -1 immediates, and we were able to reproduce a kernel crash using the BPF tests. We manually translated the C code to Racket because our verifier, Serval, currently only works on Racket code. Thanks again, - Luke [1]: https://github.com/uw-unsat/serval-bpf/blob/00838174659034e9527e67d9eccd2def2354cec6/racket/test/arm64/gen-logic-imm.py [2]: https://github.com/uw-unsat/serval-bpf/blob/00838174659034e9527e67d9eccd2def2354cec6/racket/test/arm64/logic-imm.rkt [3]: https://github.com/uw-unsat/serval-bpf/blob/00838174659034e9527e67d9eccd2def2354cec6/racket/arm64/insn.rkt#L66 [4]: https://github.com/uw-unsat/serval-bpf/blob/00838174659034e9527e67d9eccd2def2354cec6/racket/arm64/bpf_jit_comp.rkt
On Thu, May 07, 2020 at 02:48:07PM -0700, Luke Nelson wrote: > Thanks for the comments! Responses below: > > > It's a bit grotty spreading the checks out now. How about we tweak things > > slightly along the lines of: > > > > > > diff --git a/arch/arm64/kernel/insn.c b/arch/arm64/kernel/insn.c > > index 4a9e773a177f..60ec788eaf33 100644 > > --- a/arch/arm64/kernel/insn.c > > +++ b/arch/arm64/kernel/insn.c > > [...] > > Agreed; this new version looks much cleaner. I re-ran all the tests / > verification and everything seems good. Would you like me to submit a > v2 of this series with this new code? Yes, please! And please include Daniel's acks on the BPF changes too. It's a public holiday here in the UK today, but I can pick this up next week. > >> We tested the new code against llvm-mc with all 1,302 encodable 32-bit > >> logical immediates and all 5,334 encodable 64-bit logical immediates. > > > > That, on its own, is awesome information. Do you have any pointer on > > how to set this up? > > Sure! The process of running the tests is pretty involved, but I'll > describe it below and give some links here. > > We found the bugs in insn.c while adding support for logical immediates > to the BPF JIT and verifying the changes with our tool, Serval: > https://github.com/uw-unsat/serval-bpf. The basic idea for how we tested / > verified logical immediates is the following: > > First, we have a Python script [1] for generating every encodable > logical immediate and the corresponding instruction fields that encode > that immediate. The script validates the list by checking that llvm-mc > decodes each instruction back to the expected immediate. > > Next, we use the list [2] from the first step to check a Racket > translation [3] of the logical immediate encoding function in insn.c. > We found the second mask bug by noticing that some (encodable) 32-bit > immediates were being rejected by the encoding function. > > Last, we use the Racket translation of the encoding function to verify > the correctness of the BPF JIT implementation [4], i.e., the JIT > correctly compiles BPF_{AND,OR,XOR,JSET} BPF_K instructions to arm64 > instructions with equivalent semantics. We found the first bug as the > verifier complained that the function was producing invalid encodings > for 32-bit -1 immediates, and we were able to reproduce a kernel crash > using the BPF tests. > > We manually translated the C code to Racket because our verifier, Serval, > currently only works on Racket code. Nice! Two things: (1) I really think you should give a talk on this at a Linux conference (2) Did you look at any instruction generation functions other than the logical immediate encoding function? Cheers, Will
Hi Will, On Fri, May 8, 2020 at 4:47 AM Will Deacon <will@kernel.org> wrote: > > Yes, please! And please include Daniel's acks on the BPF changes too. It's a > public holiday here in the UK today, but I can pick this up next week. Thanks! > Nice! Two things: > > (1) I really think you should give a talk on this at a Linux conference That would be great, I'd be happy to give a talk on our verification work some time in the future :) > (2) Did you look at any instruction generation functions other than the > logical immediate encoding function? Other instruction generation functions are on our todo list, but we haven't got a chance to spend more time on them yet. Thanks again, - Luke
diff --git a/arch/arm64/kernel/insn.c b/arch/arm64/kernel/insn.c index 4a9e773a177f..42fad79546bb 100644 --- a/arch/arm64/kernel/insn.c +++ b/arch/arm64/kernel/insn.c @@ -1535,7 +1535,7 @@ static u32 aarch64_encode_immediate(u64 imm, u32 insn) { unsigned int immr, imms, n, ones, ror, esz, tmp; - u64 mask = ~0UL; + u64 mask; /* Can't encode full zeroes or full ones */ if (!imm || !~imm) @@ -1543,13 +1543,15 @@ static u32 aarch64_encode_immediate(u64 imm, switch (variant) { case AARCH64_INSN_VARIANT_32BIT: - if (upper_32_bits(imm)) + if (upper_32_bits(imm) || imm == 0xffffffffUL) return AARCH64_BREAK_FAULT; esz = 32; + mask = 0xffffffffUL; break; case AARCH64_INSN_VARIANT_64BIT: insn |= AARCH64_INSN_SF_BIT; esz = 64; + mask = ~0UL; break; default: pr_err("%s: unknown variant encoding %d\n", __func__, variant);