extern crate rustc_ast;
extern crate rustc_hir;
extern crate rustc_middle;
extern crate rustc_span;
use rustc_hir::def_id::DefId;
use rustc_hir::{self as hir, Expr, ExprKind, ImplItemKind, ItemKind};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_middle::ty::{Ty, TypeckResults};
use rustc_span::symbol::Symbol;
static SYM_CORE: std::sync::LazyLock<Symbol> = std::sync::LazyLock::new(|| Symbol::intern("core"));
static SYM_STD: std::sync::LazyLock<Symbol> = std::sync::LazyLock::new(|| Symbol::intern("std"));
static SYM_WRITE_BYTES: std::sync::LazyLock<Symbol> =
std::sync::LazyLock::new(|| Symbol::intern("write_bytes"));
dylint_linting::declare_late_lint! {
#[doc = include_str!("../../docs/de07_security/de0707_drop_zeroize/README.md")]
pub DE0707_DROP_ZEROIZE,
Deny,
"manual byte-zeroing in Drop may be optimized away; use `secrecy::SecretBox` or the `zeroize` crate (DE0707)"
}
fn is_zero_literal(expr: &Expr<'_>) -> bool {
if let ExprKind::Lit(lit) = expr.kind
&& let rustc_ast::ast::LitKind::Int(n, _) = lit.node
{
return n.get() == 0;
}
false
}
fn is_u8_ptr_or_ref(ty: Ty<'_>) -> bool {
let pointee = match ty.kind() {
rustc_middle::ty::TyKind::RawPtr(pointee, _) => pointee,
rustc_middle::ty::TyKind::Ref(_, pointee, _) => pointee,
_ => return false,
};
matches!(
pointee.kind(),
rustc_middle::ty::TyKind::Uint(rustc_middle::ty::UintTy::U8)
)
}
fn has_u8_element(ty: Ty<'_>) -> bool {
let ty = ty.peel_refs();
match ty.kind() {
rustc_middle::ty::TyKind::Slice(elem) | rustc_middle::ty::TyKind::Array(elem, _) => {
matches!(
elem.kind(),
rustc_middle::ty::TyKind::Uint(rustc_middle::ty::UintTy::U8)
)
}
_ => false,
}
}
fn is_ptr_write_bytes(cx: &LateContext<'_>, def_id: DefId) -> bool {
let krate = cx.tcx.crate_name(def_id.krate);
if krate != *SYM_CORE && krate != *SYM_STD {
return false;
}
cx.tcx.item_name(def_id) == *SYM_WRITE_BYTES
}
struct ZeroingVisitor<'tcx, 'cx> {
cx: &'cx LateContext<'tcx>,
typeck: &'tcx TypeckResults<'tcx>,
}
impl<'tcx> hir::intravisit::Visitor<'tcx> for ZeroingVisitor<'tcx, '_> {
fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) {
match expr.kind {
ExprKind::Assign(lhs, rhs, _) => {
if let ExprKind::Unary(hir::UnOp::Deref, inner) = lhs.kind
&& is_zero_literal(rhs)
{
let inner_ty = self.typeck.expr_ty(inner);
if is_u8_ptr_or_ref(inner_ty) {
self.cx.span_lint(DE0707_DROP_ZEROIZE, expr.span, |diag| {
diag.primary_message(
"manual byte-zeroing in `Drop::drop` may be eliminated by the optimizer (DE0707)",
);
diag.help(
"use `secrecy::SecretBox` or `zeroize`: `.zeroize()` / `#[derive(ZeroizeOnDrop)]`",
);
diag.note(
"LLVM dead-store elimination can legally remove writes that are never read; `zeroize` uses a compiler fence to prevent this",
);
});
}
}
}
ExprKind::MethodCall(seg, recv, args, _) => {
if seg.ident.name.as_str() == "fill"
&& let Some(arg) = args.first()
&& is_zero_literal(arg)
{
let method_in_std =
self.typeck
.type_dependent_def_id(expr.hir_id)
.is_some_and(|did| {
let krate = self.cx.tcx.crate_name(did.krate);
krate == *SYM_CORE || krate == *SYM_STD
});
let recv_ty = self.typeck.expr_ty_adjusted(recv);
if method_in_std && has_u8_element(recv_ty) {
self.cx.span_lint(DE0707_DROP_ZEROIZE, expr.span, |diag| {
diag.primary_message(
"manual byte-zeroing in `Drop::drop` may be eliminated by the optimizer (DE0707)",
);
diag.help(
"use `secrecy::SecretBox` or `zeroize`: `.zeroize()` / `#[derive(ZeroizeOnDrop)]`",
);
diag.note(
"LLVM dead-store elimination can legally remove writes that are never read; `zeroize` uses a compiler fence to prevent this",
);
});
}
}
}
ExprKind::Call(func, args) => {
if args.len() >= 2
&& let Some(fill_byte) = args.get(1)
&& is_zero_literal(fill_byte)
&& let ExprKind::Path(qpath) = &func.kind
&& let Some(def_id) = self.cx.qpath_res(qpath, func.hir_id).opt_def_id()
&& is_ptr_write_bytes(self.cx, def_id)
{
self.cx.span_lint(
DE0707_DROP_ZEROIZE,
expr.span,
|diag| {
diag.primary_message(
"manual byte-zeroing in `Drop::drop` may be eliminated by the optimizer (DE0707)",
);
diag.help(
"use `secrecy::SecretBox` or `zeroize`: `.zeroize()` / `#[derive(ZeroizeOnDrop)]`",
);
diag.note(
"LLVM dead-store elimination can legally remove writes that are never read; `zeroize` uses a compiler fence to prevent this",
);
},
);
}
}
_ => {}
}
hir::intravisit::walk_expr(self, expr);
}
}
impl<'tcx> LateLintPass<'tcx> for De0707DropZeroize {
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'tcx>) {
let ItemKind::Impl(impl_block) = item.kind else {
return;
};
let Some(_) = impl_block.of_trait else {
return;
};
let impl_def_id = item.owner_id.def_id;
let impl_trait_ref = cx.tcx.impl_trait_ref(impl_def_id).instantiate_identity();
let Some(drop_trait_did) = cx.tcx.lang_items().drop_trait() else {
return;
};
if impl_trait_ref.def_id != drop_trait_did {
return;
}
for item_ref in impl_block.items {
let node = cx.tcx.hir_node_by_def_id(item_ref.owner_id.def_id);
let hir::Node::ImplItem(impl_item) = node else {
continue;
};
if impl_item.ident.name.as_str() != "drop" {
continue;
}
let ImplItemKind::Fn(_, body_id) = impl_item.kind else {
continue;
};
let body = cx.tcx.hir_body(body_id);
let typeck = cx.tcx.typeck(item_ref.owner_id.def_id);
let mut visitor = ZeroingVisitor { cx, typeck };
hir::intravisit::walk_expr(&mut visitor, body.value);
}
}
}