//! Proof-carrying code checking for AArch64 VCode. use crate::ir::pcc::*; use crate::ir::types::*; use crate::ir::MemFlags; use crate::isa::aarch64::inst::args::{Cond, PairAMode, ShiftOp}; use crate::isa::aarch64::inst::regs::zero_reg; use crate::isa::aarch64::inst::Inst; use crate::isa::aarch64::inst::{ALUOp, MoveWideOp}; use crate::isa::aarch64::inst::{AMode, ExtendOp}; use crate::machinst::pcc::*; use crate::machinst::Reg; use crate::machinst::{InsnIndex, VCode}; use crate::trace; fn extend_fact(ctx: &FactContext, value: &Fact, mode: ExtendOp) -> Option { match mode { ExtendOp::UXTB => ctx.uextend(value, 8, 64), ExtendOp::UXTH => ctx.uextend(value, 16, 64), ExtendOp::UXTW => ctx.uextend(value, 32, 64), ExtendOp::UXTX => Some(value.clone()), ExtendOp::SXTB => ctx.sextend(value, 8, 64), ExtendOp::SXTH => ctx.sextend(value, 16, 64), ExtendOp::SXTW => ctx.sextend(value, 32, 64), ExtendOp::SXTX => None, } } /// Flow-state between facts. #[derive(Clone, Debug, Default)] pub struct FactFlowState { cmp_flags: Option<(Fact, Fact)>, } pub(crate) fn check( ctx: &FactContext, vcode: &mut VCode, inst_idx: InsnIndex, state: &mut FactFlowState, ) -> PccResult<()> { let inst = &vcode[inst_idx]; trace!("Checking facts on inst: {:?}", inst); // We only persist flag state for one instruction, because we // can't exhaustively enumerate all flags-effecting ops; so take // the `cmp_state` here and perhaps use it below but don't let it // remain. let cmp_flags = state.cmp_flags.take(); trace!(" * with cmp_flags = {cmp_flags:?}"); match *inst { Inst::Args { .. } => { // Defs on the args have "axiomatic facts": we trust the // ABI code to pass through the values unharmed, so the // facts given to us in the CLIF should still be true. Ok(()) } Inst::ULoad8 { rd, ref mem, flags } | Inst::SLoad8 { rd, ref mem, flags } | Inst::ULoad16 { rd, ref mem, flags } | Inst::SLoad16 { rd, ref mem, flags } | Inst::ULoad32 { rd, ref mem, flags } | Inst::SLoad32 { rd, ref mem, flags } | Inst::ULoad64 { rd, ref mem, flags } => { let access_ty = inst.mem_type().unwrap(); check_load(ctx, Some(rd.to_reg()), flags, mem, vcode, access_ty) } Inst::FpuLoad16 { ref mem, flags, .. } | Inst::FpuLoad32 { ref mem, flags, .. } | Inst::FpuLoad64 { ref mem, flags, .. } | Inst::FpuLoad128 { ref mem, flags, .. } => { let access_ty = inst.mem_type().unwrap(); check_load(ctx, None, flags, mem, vcode, access_ty) } Inst::LoadP64 { ref mem, flags, .. } => check_load_pair(ctx, flags, mem, vcode, 16), Inst::FpuLoadP64 { ref mem, flags, .. } => check_load_pair(ctx, flags, mem, vcode, 16), Inst::FpuLoadP128 { ref mem, flags, .. } => check_load_pair(ctx, flags, mem, vcode, 32), Inst::VecLoadReplicate { rn, flags, size, .. } => check_load_addr(ctx, flags, rn, vcode, size.lane_size().ty()), Inst::LoadAcquire { access_ty, rn, flags, .. } => check_load_addr(ctx, flags, rn, vcode, access_ty), Inst::Store8 { rd, ref mem, flags } | Inst::Store16 { rd, ref mem, flags } | Inst::Store32 { rd, ref mem, flags } | Inst::Store64 { rd, ref mem, flags } => { let access_ty = inst.mem_type().unwrap(); check_store(ctx, Some(rd), flags, mem, vcode, access_ty) } Inst::FpuStore16 { ref mem, flags, .. } | Inst::FpuStore32 { ref mem, flags, .. } | Inst::FpuStore64 { ref mem, flags, .. } | Inst::FpuStore128 { ref mem, flags, .. } => { let access_ty = inst.mem_type().unwrap(); check_store(ctx, None, flags, mem, vcode, access_ty) } Inst::StoreP64 { ref mem, flags, .. } => check_store_pair(ctx, flags, mem, vcode, 16), Inst::FpuStoreP64 { ref mem, flags, .. } => check_store_pair(ctx, flags, mem, vcode, 16), Inst::FpuStoreP128 { ref mem, flags, .. } => check_store_pair(ctx, flags, mem, vcode, 32), Inst::StoreRelease { access_ty, rn, flags, .. } => check_store_addr(ctx, flags, rn, vcode, access_ty), Inst::AluRRR { alu_op: ALUOp::Add | ALUOp::AddS, size, rd, rn, rm, } => check_binop(ctx, vcode, 64, rd, rn, rm, |rn, rm| { clamp_range( ctx, 64, size.bits().into(), ctx.add(rn, rm, size.bits().into()), ) }), Inst::AluRRImm12 { alu_op: ALUOp::Add | ALUOp::AddS, size, rd, rn, imm12, } => check_unop(ctx, vcode, 64, rd, rn, |rn| { let imm12: i64 = imm12.value().into(); clamp_range( ctx, 64, size.bits().into(), ctx.offset(&rn, size.bits().into(), imm12), ) }), Inst::AluRRImm12 { alu_op: ALUOp::Sub, size, rd, rn, imm12, } => check_unop(ctx, vcode, 64, rd, rn, |rn| { let imm12: i64 = imm12.value().into(); clamp_range( ctx, 64, size.bits().into(), ctx.offset(&rn, size.bits().into(), -imm12), ) }), Inst::AluRRR { alu_op: ALUOp::Sub, size, rd, rn, rm, } => check_binop(ctx, vcode, 64, rd, rn, rm, |rn, rm| { if let Some(k) = rm.as_const(64) { clamp_range( ctx, 64, size.bits().into(), ctx.offset(rn, size.bits().into(), -(k as i64)), ) } else { clamp_range(ctx, 64, size.bits().into(), None) } }), Inst::AluRRRShift { alu_op: ALUOp::Add | ALUOp::AddS, size, rd, rn, rm, shiftop, } if shiftop.op() == ShiftOp::LSL && has_fact(vcode, rn) && has_fact(vcode, rm) => { check_binop(ctx, vcode, 64, rd, rn, rm, |rn, rm| { let rm_shifted = fail_if_missing(ctx.shl( &rm, size.bits().into(), shiftop.amt().value().into(), ))?; clamp_range( ctx, 64, size.bits().into(), ctx.add(&rn, &rm_shifted, size.bits().into()), ) }) } Inst::AluRRRExtend { alu_op: ALUOp::Add | ALUOp::AddS, size, rd, rn, rm, extendop, } if has_fact(vcode, rn) && has_fact(vcode, rm) => { check_binop(ctx, vcode, 64, rd, rn, rm, |rn, rm| { let rm_extended = fail_if_missing(extend_fact(ctx, rm, extendop))?; clamp_range( ctx, 64, size.bits().into(), ctx.add(&rn, &rm_extended, size.bits().into()), ) }) } Inst::AluRRImmShift { alu_op: ALUOp::Lsl, size, rd, rn, immshift, } if has_fact(vcode, rn) => check_unop(ctx, vcode, 64, rd, rn, |rn| { clamp_range( ctx, 64, size.bits().into(), ctx.shl(&rn, size.bits().into(), immshift.value().into()), ) }), Inst::Extend { rd, rn, signed: false, from_bits, to_bits, } if has_fact(vcode, rn) => check_unop(ctx, vcode, 64, rd, rn, |rn| { clamp_range( ctx, 64, to_bits.into(), ctx.uextend(&rn, from_bits.into(), to_bits.into()), ) }), Inst::AluRRR { alu_op: ALUOp::SubS, size, rd, rn, rm, } if rd.to_reg() == zero_reg() => { // Compare. let rn = get_fact_or_default(vcode, rn, size.bits().into()); let rm = get_fact_or_default(vcode, rm, size.bits().into()); state.cmp_flags = Some((rn, rm)); Ok(()) } Inst::AluRRImmLogic { alu_op: ALUOp::Orr, size, rd, rn, imml, } if rn == zero_reg() => { let constant = imml.value(); check_constant(ctx, vcode, rd, size.bits().into(), constant) } Inst::AluRRR { rd, size, .. } | Inst::AluRRImm12 { rd, size, .. } | Inst::AluRRRShift { rd, size, .. } | Inst::AluRRRExtend { rd, size, .. } | Inst::AluRRImmLogic { rd, size, .. } | Inst::AluRRImmShift { rd, size, .. } => check_output(ctx, vcode, rd, &[], |_vcode| { clamp_range(ctx, 64, size.bits().into(), None) }), Inst::Extend { rd, from_bits, to_bits, .. } => check_output(ctx, vcode, rd, &[], |_vcode| { clamp_range(ctx, to_bits.into(), from_bits.into(), None) }), Inst::MovWide { op: MoveWideOp::MovZ, imm, size: _, rd, } => { let constant = u64::from(imm.bits) << (imm.shift * 16); check_constant(ctx, vcode, rd, 64, constant) } Inst::MovWide { op: MoveWideOp::MovN, imm, size, rd, } => { let constant = !(u64::from(imm.bits) << (imm.shift * 16)) & size.max_value(); check_constant(ctx, vcode, rd, 64, constant) } Inst::MovK { rd, rn, imm, .. } => { let input = get_fact_or_default(vcode, rn, 64); if let Some(input_constant) = input.as_const(64) { let mask = 0xffff << (imm.shift * 16); let constant = u64::from(imm.bits) << (imm.shift * 16); let constant = (input_constant & !mask) | constant; check_constant(ctx, vcode, rd, 64, constant) } else { check_output(ctx, vcode, rd, &[], |_vcode| { Ok(Some(Fact::max_range_for_width(64))) }) } } Inst::CSel { rd, cond, rn, rm } if (cond == Cond::Hs || cond == Cond::Hi) && cmp_flags.is_some() => { let (cmp_lhs, cmp_rhs) = cmp_flags.unwrap(); trace!("CSel: cmp {cond:?} ({cmp_lhs:?}, {cmp_rhs:?})"); check_output(ctx, vcode, rd, &[], |vcode| { // We support transitivity-based reasoning. If the // comparison establishes that // // (x+K1) <= (y+K2) // // then on the true-side of the select we can edit the maximum // in a DynamicMem or DynamicRange by replacing x's with y's // with appropriate offsets -- this widens the range. // // Likewise, on the false-side of the select we can // replace y's with x's -- this also widens the range. On // the false side we know the inequality is strict, so we // can offset by one. // True side: lhs >= rhs (Hs) or lhs > rhs (Hi). let rn = get_fact_or_default(vcode, rn, 64); let lhs_kind = match cond { Cond::Hs => InequalityKind::Loose, Cond::Hi => InequalityKind::Strict, _ => unreachable!(), }; let rn = ctx.apply_inequality(&rn, &cmp_lhs, &cmp_rhs, lhs_kind); // false side: rhs < lhs (Hs) or rhs <= lhs (Hi). let rm = get_fact_or_default(vcode, rm, 64); let rhs_kind = match cond { Cond::Hs => InequalityKind::Strict, Cond::Hi => InequalityKind::Loose, _ => unreachable!(), }; let rm = ctx.apply_inequality(&rm, &cmp_rhs, &cmp_lhs, rhs_kind); let union = ctx.union(&rn, &rm); // Union the two facts. clamp_range(ctx, 64, 64, union) }) } _ if vcode.inst_defines_facts(inst_idx) => Err(PccError::UnsupportedFact), _ => Ok(()), } } fn check_load( ctx: &FactContext, rd: Option, flags: MemFlags, addr: &AMode, vcode: &VCode, ty: Type, ) -> PccResult<()> { let result_fact = rd.and_then(|rd| vcode.vreg_fact(rd.into())); let bits = u16::try_from(ty.bits()).unwrap(); check_addr( ctx, flags, addr, vcode, ty, LoadOrStore::Load { result_fact, from_bits: bits, to_bits: bits, }, ) } fn check_store( ctx: &FactContext, rd: Option, flags: MemFlags, addr: &AMode, vcode: &VCode, ty: Type, ) -> PccResult<()> { let stored_fact = rd.and_then(|rd| vcode.vreg_fact(rd.into())); check_addr( ctx, flags, addr, vcode, ty, LoadOrStore::Store { stored_fact }, ) } fn check_addr<'a>( ctx: &FactContext, flags: MemFlags, addr: &AMode, vcode: &VCode, ty: Type, op: LoadOrStore<'a>, ) -> PccResult<()> { if !flags.checked() { return Ok(()); } trace!("check_addr: {:?}", addr); let check = |addr: &Fact, ty: Type| -> PccResult<()> { match op { LoadOrStore::Load { result_fact, from_bits, to_bits, } => { let loaded_fact = clamp_range(ctx, to_bits, from_bits, ctx.load(addr, ty)?.cloned())?; trace!( "checking a load: loaded_fact = {loaded_fact:?} result_fact = {result_fact:?}" ); if ctx.subsumes_fact_optionals(loaded_fact.as_ref(), result_fact) { Ok(()) } else { Err(PccError::UnsupportedFact) } } LoadOrStore::Store { stored_fact } => ctx.store(addr, ty, stored_fact), } }; match addr { &AMode::RegReg { rn, rm } => { let rn = get_fact_or_default(vcode, rn, 64); let rm = get_fact_or_default(vcode, rm, 64); let sum = fail_if_missing(ctx.add(&rn, &rm, 64))?; trace!("rn = {rn:?} rm = {rm:?} sum = {sum:?}"); check(&sum, ty) } &AMode::RegScaled { rn, rm } => { let rn = get_fact_or_default(vcode, rn, 64); let rm = get_fact_or_default(vcode, rm, 64); let rm_scaled = fail_if_missing(ctx.scale(&rm, 64, ty.bytes()))?; let sum = fail_if_missing(ctx.add(&rn, &rm_scaled, 64))?; check(&sum, ty) } &AMode::RegScaledExtended { rn, rm, extendop } => { let rn = get_fact_or_default(vcode, rn, 64); let rm = get_fact_or_default(vcode, rm, 64); let rm_extended = fail_if_missing(extend_fact(ctx, &rm, extendop))?; let rm_scaled = fail_if_missing(ctx.scale(&rm_extended, 64, ty.bytes()))?; let sum = fail_if_missing(ctx.add(&rn, &rm_scaled, 64))?; check(&sum, ty) } &AMode::RegExtended { rn, rm, extendop } => { let rn = get_fact_or_default(vcode, rn, 64); let rm = get_fact_or_default(vcode, rm, 64); let rm_extended = fail_if_missing(extend_fact(ctx, &rm, extendop))?; let sum = fail_if_missing(ctx.add(&rn, &rm_extended, 64))?; check(&sum, ty)?; Ok(()) } &AMode::Unscaled { rn, simm9 } => { let rn = get_fact_or_default(vcode, rn, 64); let sum = fail_if_missing(ctx.offset(&rn, 64, simm9.value.into()))?; check(&sum, ty) } &AMode::UnsignedOffset { rn, uimm12 } => { let rn = get_fact_or_default(vcode, rn, 64); // N.B.: the architecture scales the immediate in the // encoded instruction by the size of the loaded type, so // e.g. an offset field of 4095 can mean a load of offset // 32760 (= 4095 * 8) for I64s. The `UImm12Scaled` type // stores the *scaled* value, so we don't need to multiply // (again) by the type's size here. let offset: u64 = uimm12.value().into(); // This `unwrap()` will always succeed because the value // will always be positive and much smaller than // `i64::MAX` (see above). let sum = fail_if_missing(ctx.offset(&rn, 64, i64::try_from(offset).unwrap()))?; check(&sum, ty) } &AMode::Label { .. } | &AMode::Const { .. } => { // Always accept: labels and constants must be within the // generated code (else they won't be resolved). Ok(()) } &AMode::RegOffset { rn, off, .. } => { let rn = get_fact_or_default(vcode, rn, 64); let sum = fail_if_missing(ctx.offset(&rn, 64, off))?; check(&sum, ty) } &AMode::SPOffset { .. } | &AMode::FPOffset { .. } | &AMode::IncomingArg { .. } | &AMode::SlotOffset { .. } | &AMode::SPPostIndexed { .. } | &AMode::SPPreIndexed { .. } => { // We trust ABI code (for now!) and no lowering rules // lower input value accesses directly to these. Ok(()) } } } fn check_load_pair( _ctx: &FactContext, _flags: MemFlags, _addr: &PairAMode, _vcode: &VCode, _size: u8, ) -> PccResult<()> { Err(PccError::UnimplementedInst) } fn check_store_pair( _ctx: &FactContext, _flags: MemFlags, _addr: &PairAMode, _vcode: &VCode, _size: u8, ) -> PccResult<()> { Err(PccError::UnimplementedInst) } fn check_load_addr( ctx: &FactContext, flags: MemFlags, reg: Reg, vcode: &VCode, ty: Type, ) -> PccResult<()> { if !flags.checked() { return Ok(()); } let fact = get_fact_or_default(vcode, reg, 64); let _output_fact = ctx.load(&fact, ty)?; Ok(()) } fn check_store_addr( ctx: &FactContext, flags: MemFlags, reg: Reg, vcode: &VCode, ty: Type, ) -> PccResult<()> { if !flags.checked() { return Ok(()); } let fact = get_fact_or_default(vcode, reg, 64); let _output_fact = ctx.store(&fact, ty, None)?; Ok(()) }