// Licensed under the Apache License, Version 2.0 or the MIT License. // Copyright (C) 2023-2024 Ant Group. #![feature(rustc_private)] extern crate rustc_ast; extern crate rustc_driver; extern crate rustc_hir; extern crate rustc_lint; extern crate rustc_middle; extern crate rustc_session; extern crate rustc_span; mod conf; use std::collections::HashSet; pub use conf::init as init_conf; pub use conf::lookup_conf_file; use rustc_ast::AttrKind; use rustc_middle::mir::{ Constant, InlineAsmOperand, LocalDecl, Operand, Rvalue, Statement, StatementKind, Terminator, TerminatorKind, }; use rustc_middle::query::Key; use rustc_middle::ty::{ImplSubject, InstanceDef, TyCtxt, TyKind, WithOptConstParam}; use rustc_span::def_id::{DefId, LocalDefId, LOCAL_CRATE}; use rustc_span::Span; const TOOL_NAME: &'static str = "component_access_control"; const CONTROLLED_ATTR: &'static str = "controlled"; pub fn enter_analysis<'tcx>(tcx: TyCtxt<'tcx>) { for mir_key in tcx.mir_keys(()) { check_body_mir(mir_key.clone(), tcx) } } fn check_body_mir(mir_key: LocalDefId, tcx: TyCtxt<'_>) { let def_id = WithOptConstParam::unknown(mir_key.to_def_id()); // For const function/block, instance_mir returns mir_for_ctfe. // For normal function, instance_mir returns optimized_mir. let body = tcx.instance_mir(InstanceDef::Item(def_id)); let mut checked_def_ids = HashSet::new(); for basic_block_data in body.basic_blocks.iter() { // This check based on the assumption that any **DIRECT** visit to // static variables or functions can be found in Operand. // FIXME: is this true? for statement in &basic_block_data.statements { check_statement(statement, tcx, &mut checked_def_ids); } if let Some(terminator) = &basic_block_data.terminator { check_terminator(terminator, tcx, &mut checked_def_ids); } } // For some special cases, assign a function to a function pointer may not exist in statements, // while a local decl with type of the function exist. So we further check each local decl to // avoid missing any entry points. for local_decl in body.local_decls.iter() { check_local_decl(local_decl, tcx, &checked_def_ids) } } fn check_statement(statement: &Statement, tcx: TyCtxt<'_>, checked_def_ids: &mut HashSet) { // FIXME: operand only exist in assign statement? let mut def_paths = Vec::new(); if let StatementKind::Assign(assignment) = &statement.kind { let rvalue = &assignment.1; match rvalue { Rvalue::Use(operand) | Rvalue::Repeat(operand, _) | Rvalue::Cast(_, operand, _) | Rvalue::UnaryOp(_, operand) | Rvalue::ShallowInitBox(operand, _) => { check_invalid_operand(operand, tcx, &mut def_paths, checked_def_ids); } Rvalue::BinaryOp(_, two_operands) | Rvalue::CheckedBinaryOp(_, two_operands) => { check_invalid_operand(&two_operands.0, tcx, &mut def_paths, checked_def_ids); check_invalid_operand(&two_operands.1, tcx, &mut def_paths, checked_def_ids); } Rvalue::Aggregate(_, operands) => { for operand in operands { check_invalid_operand(operand, tcx, &mut def_paths, checked_def_ids); } } _ => {} } } let crate_symbol = tcx.crate_name(LOCAL_CRATE); let crate_name = crate_symbol.as_str(); emit_note(tcx, statement.source_info.span, crate_name, def_paths) } fn check_terminator( terminator: &Terminator, tcx: TyCtxt<'_>, checked_def_ids: &mut HashSet, ) { let mut def_paths = Vec::new(); match &terminator.kind { TerminatorKind::SwitchInt { discr: operand, .. } | TerminatorKind::DropAndReplace { value: operand, .. } | TerminatorKind::Assert { cond: operand, .. } | TerminatorKind::Yield { value: operand, .. } => { check_invalid_operand(operand, tcx, &mut def_paths, checked_def_ids); } TerminatorKind::Call { func, args, .. } => { check_invalid_operand(func, tcx, &mut def_paths, checked_def_ids); for arg in args { check_invalid_operand(arg, tcx, &mut def_paths, checked_def_ids); } } TerminatorKind::InlineAsm { operands, .. } => { for asm_operand in operands { check_inline_asm_operand(&asm_operand, tcx, &mut def_paths, checked_def_ids); } } _ => {} } let crate_symbol = tcx.crate_name(LOCAL_CRATE); let crate_name = crate_symbol.as_str(); emit_note(tcx, terminator.source_info.span, crate_name, def_paths) } fn check_local_decl(local_decl: &LocalDecl<'_>, tcx: TyCtxt<'_>, checked_def_ids: &HashSet) { let ty = local_decl.ty; let def_id = if let TyKind::FnDef(def_id, ..) = ty.kind() { // func def *def_id } else { return; }; if checked_def_ids.contains(&def_id) { return; } let crate_symbol = tcx.crate_name(LOCAL_CRATE); let crate_name = crate_symbol.as_str(); if let Some(def_path) = def_path_if_invalid_access(def_id, tcx) { emit_note(tcx, local_decl.source_info.span, crate_name, vec![def_path]); } } fn check_inline_asm_operand( asm_operand: &InlineAsmOperand<'_>, tcx: TyCtxt<'_>, def_paths: &mut Vec, checked_def_ids: &mut HashSet, ) { match asm_operand { InlineAsmOperand::In { value: operand, .. } | InlineAsmOperand::InOut { in_value: operand, .. } => { check_invalid_operand(operand, tcx, def_paths, checked_def_ids); } InlineAsmOperand::Const { value } | InlineAsmOperand::SymFn { value } => { check_constant(value, tcx, def_paths, checked_def_ids); } _ => {} } } /// check whether visiting the operand in local crate is valid. /// if the operand is invalid, add the def_path to def_paths. /// The operand is invalid only when following four points are all satisfied. /// 1. The operand represents a static variable or a func(the first argument can not be self or its variants). /// 2. The operand is not defined in local crate. /// 3. The operand is marked with #[component_access_control::controlled] /// 4. Local crate is not in the whitelist to visit the operand. fn check_invalid_operand( operand: &Operand, tcx: TyCtxt<'_>, def_paths: &mut Vec, checked_def_ids: &mut HashSet, ) { if let Operand::Constant(constant) = operand { check_constant(&constant, tcx, def_paths, checked_def_ids); } else { return; }; } fn check_constant( constant: &Constant<'_>, tcx: TyCtxt<'_>, def_paths: &mut Vec, checked_def_ids: &mut HashSet, ) { // get def_id of Constant and func let def_id = if let Some(def_id) = constant.check_static_ptr(tcx) { // static variable def_id } else { let ty = constant.ty(); if let TyKind::FnDef(def_id, ..) = ty.kind() { // func def *def_id } else { return; } }; checked_def_ids.insert(def_id); if let Some(def_path) = def_path_if_invalid_access(def_id, tcx) { def_paths.push(def_path); } } fn def_path_if_invalid_access(def_id: DefId, tcx: TyCtxt<'_>) -> Option { if def_id.is_local() { return None; } if !contains_controlled_attr(def_id, tcx) { return None; } def_path_if_not_in_whitelist(def_id, tcx) } /// check whether the def_id is in white list. /// If the def_id is **NOT** in white list, return the def_path fn def_path_if_not_in_whitelist(def_id: DefId, tcx: TyCtxt<'_>) -> Option { let crate_symbol = tcx.crate_name(LOCAL_CRATE); let crate_name = crate_symbol.as_str(); let def_path_str = def_path_for_def_id(tcx, def_id); if conf::CONFIG .get() .unwrap() .allow_access(crate_name, &def_path_str) { None } else { Some(def_path_str) } } /// if the def_id has attribute component_access_control::controlled, return true, else return false fn contains_controlled_attr(def_id: DefId, tcx: TyCtxt<'_>) -> bool { for attr in tcx.get_attrs_unchecked(def_id) { if let AttrKind::Normal(normal_attr) = &attr.kind { let path_segments = &normal_attr.item.path.segments; if path_segments.len() != 2 { return false; } let segment_strs: Vec<_> = path_segments .iter() .map(|segment| segment.ident.as_str()) .collect(); if segment_strs[0] == TOOL_NAME && segment_strs[1] == CONTROLLED_ATTR { return true; } } } false } fn def_path_for_def_id(tcx: TyCtxt<'_>, def_id: DefId) -> String { match tcx.impl_of_method(def_id) { None => common_def_path_str(tcx, def_id), Some(impl_def_id) => def_path_str_for_impl(tcx, def_id, impl_def_id), } } /// def path for function, type, static variables and trait methods fn common_def_path_str(tcx: TyCtxt<'_>, def_id: DefId) -> String { // This function is like def_path_debug_str without noisy info let def_path = tcx.def_path(def_id); let crate_name = tcx.crate_name(def_path.krate); format!("{}{}", crate_name, def_path.to_string_no_crate_verbose()) } /// def path for impl, if the impl is not for trait. fn def_path_str_for_impl(tcx: TyCtxt<'_>, def_id: DefId, impl_def_id: DefId) -> String { let item_name = tcx.item_name(def_id).to_string(); let impl_subject = tcx.impl_subject(impl_def_id); if let ImplSubject::Inherent(impl_ty) = impl_subject { let impl_ty_def_id = impl_ty.ty_adt_id().expect("Method should impl an adt type"); let impl_ty_name = common_def_path_str(tcx, impl_ty_def_id); return format!("{impl_ty_name}::{item_name}"); } // impl trait goes here, which is impossible unreachable!() } fn emit_note(tcx: TyCtxt<'_>, span: Span, crate_name: &str, def_paths: Vec) { if def_paths.len() > 0 { let sess = tcx.sess; const TITLE: &'static str = "access controlled entry point is disallowed"; let def_path = def_paths.join(", "); let warning_message = format!("access {} in {}", def_path, crate_name); sess.struct_span_warn(span, TITLE) .note(warning_message) .emit(); } }