use crate::FieldType;
use crate::JavaStr;
use crate::JavaString;
use crate::attributes::{Attribute, ExceptionTableEntry, Instruction};
use crate::class_file::ClassFile;
use crate::method::Method;
use crate::method_access_flags::MethodAccessFlags;
use crate::verifiers::bytecode::config::VerifierConfig;
use crate::verifiers::bytecode::control_flow::{
CodeInfo, compute_successors, validate_exception_table,
};
use crate::verifiers::bytecode::diagnostics::{VerificationDiagnostic, VerificationTrace};
use crate::verifiers::bytecode::frame::Frame;
use crate::verifiers::bytecode::handlers;
use crate::verifiers::bytecode::handlers::references::ConstantPoolResolver;
use crate::verifiers::bytecode::stackmap::DecodedStackMapTable;
use crate::verifiers::bytecode::type_system::VerificationType;
use crate::verifiers::context::VerificationContext;
use crate::verifiers::error::{Result, VerifyError};
use ahash::AHashSet;
use std::io::Cursor;
type CodeAttributeParts<'a> = (
&'a Vec<Instruction>,
u16,
u16,
&'a Vec<Attribute>,
&'a Vec<ExceptionTableEntry>,
);
#[derive(Debug)]
pub enum FastPathResult {
Success,
Failed(VerifyError),
NeedsFallback(String),
}
pub struct FastPathVerifier<'a, C: VerificationContext> {
class_file: &'a ClassFile<'a>,
method: &'a Method,
context: &'a C,
config: &'a VerifierConfig,
code: &'a Vec<Instruction>,
exception_table: &'a Vec<ExceptionTableEntry>,
max_stack: u16,
max_locals: u16,
code_info: CodeInfo,
return_type: Option<FieldType>,
major_version: u16,
current_class: String,
method_name: String,
method_descriptor: String,
stack_map_table: DecodedStackMapTable,
jump_targets: AHashSet<u16>,
handler_entries: AHashSet<u16>,
trace: VerificationTrace,
}
impl<C: VerificationContext> std::fmt::Debug for FastPathVerifier<'_, C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FastPathVerifier")
.field("current_class", &self.current_class)
.field("method_name", &self.method_name)
.field("method_descriptor", &self.method_descriptor)
.field("max_stack", &self.max_stack)
.field("max_locals", &self.max_locals)
.field("major_version", &self.major_version)
.finish_non_exhaustive()
}
}
impl<'a, C: VerificationContext> FastPathVerifier<'a, C> {
pub fn new(
class_file: &'a ClassFile<'a>,
method: &'a Method,
context: &'a C,
config: &'a VerifierConfig,
) -> Result<Self> {
let (code, max_stack, max_locals, code_attributes, exception_table) =
Self::extract_code_attribute(method)?;
let code_info = Self::build_code_info(code)?;
let (current_class, method_name, method_descriptor) =
Self::extract_method_info(class_file, method)?;
let method_descriptor_js = JavaStr::cow_from_str(&method_descriptor);
let (_, return_type) = FieldType::parse_method_descriptor(&method_descriptor_js)
.map_err(|e| VerifyError::ClassFormatError(e.to_string()))?;
let major_version = class_file.version.major();
let initial_frame = Self::create_initial_frame_static(
method,
class_file,
¤t_class,
&method_name,
max_locals,
max_stack,
)?;
let stack_map_table =
Self::decode_stack_map_table(code_attributes, &initial_frame, class_file, max_stack)?;
stack_map_table.validate_offsets(|offset| code_info.is_valid_offset(offset))?;
let (jump_targets, handler_entries) =
Self::collect_control_flow_targets(code, exception_table, &code_info);
let trace = VerificationTrace::new(config.verbose() || config.trace());
Ok(Self {
class_file,
method,
context,
config,
code,
exception_table,
max_stack,
max_locals,
code_info,
return_type,
major_version,
current_class,
method_name,
method_descriptor,
stack_map_table,
jump_targets,
handler_entries,
trace,
})
}
fn extract_code_attribute(method: &'a Method) -> Result<CodeAttributeParts<'a>> {
method
.attributes
.iter()
.find_map(|attr| match attr {
Attribute::Code {
code,
max_stack,
max_locals,
attributes,
exception_table,
..
} => Some((code, *max_stack, *max_locals, attributes, exception_table)),
_ => None,
})
.ok_or_else(|| {
VerifyError::ClassFormatError("Method has no Code attribute".to_string())
})
}
fn build_code_info(code: &[Instruction]) -> Result<CodeInfo> {
let mut instruction_offsets = Vec::with_capacity(code.len());
let mut cursor = Cursor::new(Vec::new());
for instruction in code {
let offset = u16::try_from(cursor.position())?;
instruction_offsets.push(offset);
instruction
.to_bytes(&mut cursor)
.map_err(|e| VerifyError::ClassFormatError(e.to_string()))?;
}
let code_length = u16::try_from(cursor.position())?;
Ok(CodeInfo::new(instruction_offsets, code_length))
}
fn extract_method_info(
class_file: &ClassFile<'_>,
method: &Method,
) -> Result<(String, String, String)> {
let current_class = class_file
.constant_pool
.try_get_class(class_file.this_class)
.map_err(|e| VerifyError::ClassFormatError(e.to_string()))?
.to_string();
let method_name = class_file
.constant_pool
.try_get_utf8(method.name_index)
.map_err(|e| VerifyError::ClassFormatError(e.to_string()))?
.to_string();
let method_descriptor = class_file
.constant_pool
.try_get_utf8(method.descriptor_index)
.map_err(|e| VerifyError::ClassFormatError(e.to_string()))?
.to_string();
Ok((current_class, method_name, method_descriptor))
}
fn decode_stack_map_table(
code_attributes: &[Attribute],
initial_frame: &Frame,
class_file: &ClassFile<'_>,
max_stack: u16,
) -> Result<DecodedStackMapTable> {
let stack_frames = code_attributes.iter().find_map(|attr| {
if let Attribute::StackMapTable { frames, .. } = attr {
Some(frames)
} else {
None
}
});
if let Some(frames) = stack_frames {
DecodedStackMapTable::decode(frames, initial_frame, class_file, max_stack)
} else {
Ok(DecodedStackMapTable::empty())
}
}
fn collect_control_flow_targets(
code: &[Instruction],
exception_table: &[ExceptionTableEntry],
code_info: &CodeInfo,
) -> (AHashSet<u16>, AHashSet<u16>) {
let mut jump_targets = AHashSet::default();
let mut handler_entries = AHashSet::default();
for handler in exception_table {
handler_entries.insert(handler.handler_pc);
}
let code_length = code_info.code_length();
for (index, instruction) in code.iter().enumerate() {
let offset = code_info.offset_at(index).unwrap_or(0);
let next_offset = code_info.offset_at(index + 1).unwrap_or(code_length);
if let Ok((successors, _)) =
compute_successors(offset, instruction, next_offset, code_info)
{
for succ in successors {
if succ != next_offset {
jump_targets.insert(succ);
}
}
}
}
(jump_targets, handler_entries)
}
fn create_initial_frame_static(
method: &Method,
class_file: &ClassFile<'_>,
current_class: &str,
method_name: &str,
max_locals: u16,
max_stack: u16,
) -> Result<Frame> {
let mut frame = Frame::new(max_locals as usize, max_stack as usize);
let mut local_index = 0;
if !method.access_flags.contains(MethodAccessFlags::STATIC) {
if method_name == "<init>" {
frame.set_local(local_index, VerificationType::UninitializedThis)?;
} else {
frame.set_local(
local_index,
VerificationType::Object(JavaString::from(current_class)),
)?;
}
local_index += 1;
}
let descriptor = class_file
.constant_pool
.try_get_utf8(method.descriptor_index)
.map_err(|e| VerifyError::ClassFormatError(e.to_string()))?;
let (parameters, _) = FieldType::parse_method_descriptor(descriptor)
.map_err(|e| VerifyError::ClassFormatError(e.to_string()))?;
for param in parameters {
if local_index >= max_locals {
return Err(VerifyError::VerifyError(
"Arguments exceed max_locals".to_string(),
));
}
let v_type = VerificationType::from_field_type(¶m);
if v_type.is_category2() {
frame.set_local_category2(local_index, v_type)?;
local_index += 2;
} else {
frame.set_local(local_index, v_type)?;
local_index += 1;
}
}
Ok(frame)
}
fn create_initial_frame(&self) -> Result<Frame> {
Self::create_initial_frame_static(
self.method,
self.class_file,
&self.current_class,
&self.method_name,
self.max_locals,
self.max_stack,
)
}
pub fn verify(&mut self) -> FastPathResult {
if let Err(e) = validate_exception_table(self.exception_table, &self.code_info) {
return FastPathResult::Failed(e);
}
if self.config.requires_stackmap(self.major_version) && self.stack_map_table.is_empty() {
if !self.code.is_empty() && self.has_control_flow() {
if self.config.allows_inference_fallback() {
return FastPathResult::NeedsFallback(
"StackMapTable required but not present".to_string(),
);
}
return FastPathResult::Failed(VerifyError::VerifyError(
"StackMapTable required for class file version 50+ with control flow"
.to_string(),
));
}
}
let initial_frame = match self.create_initial_frame() {
Ok(f) => f,
Err(e) => return FastPathResult::Failed(e),
};
match self.verify_with_stackmaps(&initial_frame) {
Ok(()) => FastPathResult::Success,
Err(error) => {
if self.config.allows_inference_fallback() && Self::is_fallback_eligible(&error) {
FastPathResult::NeedsFallback(format!("Fast path failed: {error}"))
} else {
FastPathResult::Failed(error)
}
}
}
}
fn has_control_flow(&self) -> bool {
!self.jump_targets.is_empty() || !self.handler_entries.is_empty()
}
fn is_fallback_eligible(error: &VerifyError) -> bool {
matches!(error,
VerifyError::VerifyError(msg) if msg.contains("merge point") || msg.contains("StackMapTable")
)
}
fn verify_with_stackmaps(&mut self, initial_frame: &Frame) -> Result<()> {
let mut anchor_states: Vec<Option<Frame>> = vec![None; self.code_info.instruction_count()];
anchor_states[0] = Some(initial_frame.clone());
for decoded in self.stack_map_table.frames() {
if let Some(index) = self.code_info.index_at(decoded.offset) {
let frame = self
.stack_map_table
.to_frame(decoded, self.max_locals, self.max_stack);
anchor_states[index] = Some(frame);
}
}
let mut worklist: Vec<usize> = vec![0];
let mut visited = vec![false; self.code_info.instruction_count()];
while let Some(start_index) = worklist.pop() {
if visited[start_index] {
continue;
}
let start_frame = anchor_states[start_index].clone().ok_or_else(|| {
VerifyError::VerifyError(format!(
"No frame at anchor index {} (offset {:?})",
start_index,
self.code_info.offset_at(start_index)
))
})?;
let mut current_frame = start_frame;
let mut index = start_index;
loop {
if index >= self.code.len() {
break;
}
let offset = self.code_info.offset_at(index).ok_or_else(|| {
VerifyError::VerifyError(format!("Invalid instruction index {index}"))
})?;
if index != start_index && self.is_anchor_point(offset) {
if let Some(decoded) = self.stack_map_table.get(offset) {
let expected =
self.stack_map_table
.to_frame(decoded, self.max_locals, self.max_stack);
self.validate_frame_compatibility(¤t_frame, &expected, offset)?;
}
if !visited[index] {
worklist.push(index);
}
break;
}
visited[index] = true;
let instruction = &self.code[index];
if self.trace.is_enabled() {
let pre_frame = current_frame.clone();
self.trace.log_anchor(offset, &pre_frame);
}
let next_offset = self
.code_info
.offset_at(index + 1)
.unwrap_or(self.code_info.code_length());
let (next_frame, successors, falls_through) =
self.execute_instruction(offset, index, instruction, current_frame.clone())?;
self.process_exception_handlers(
offset,
&next_frame,
&mut anchor_states,
&mut worklist,
)?;
for succ_offset in &successors {
if *succ_offset == next_offset && falls_through {
continue;
}
let Some(succ_index) = self.code_info.index_at(*succ_offset) else {
continue;
};
if visited[succ_index] {
continue;
}
self.handle_successor(
succ_index,
*succ_offset,
&next_frame,
&mut anchor_states,
)?;
worklist.push(succ_index);
}
if falls_through {
current_frame = next_frame;
index += 1;
} else {
break;
}
}
}
Ok(())
}
fn is_anchor_point(&self, offset: u16) -> bool {
self.stack_map_table.has_frame_at(offset)
|| self.jump_targets.contains(&offset)
|| self.handler_entries.contains(&offset)
}
fn is_merge_point(&self, offset: u16) -> bool {
self.jump_targets.contains(&offset) || self.handler_entries.contains(&offset)
}
fn handle_successor(
&self,
succ_index: usize,
succ_offset: u16,
next_frame: &Frame,
anchor_states: &mut [Option<Frame>],
) -> Result<()> {
if let Some(decoded) = self.stack_map_table.get(succ_offset) {
let expected = self
.stack_map_table
.to_frame(decoded, self.max_locals, self.max_stack);
self.validate_frame_compatibility(next_frame, &expected, succ_offset)?;
anchor_states[succ_index] = Some(expected);
} else if self.is_merge_point(succ_offset) {
if self.config.fallback_strategy
== crate::verifiers::bytecode::config::FallbackStrategy::Strict
{
return Err(VerifyError::VerifyError(format!(
"Merge point at offset {succ_offset} requires StackMapTable frame"
)));
}
if let Some(existing) = &mut anchor_states[succ_index] {
existing.merge(next_frame, self.context)?;
} else {
anchor_states[succ_index] = Some(next_frame.clone());
}
} else {
anchor_states[succ_index] = Some(next_frame.clone());
}
Ok(())
}
fn validate_frame_compatibility(
&self,
computed: &Frame,
expected: &Frame,
offset: u16,
) -> Result<()> {
if computed.stack.len() != expected.stack.len() {
let computed_len = computed.stack.len();
let expected_len = expected.stack.len();
return Err(VerifyError::VerifyError(format!(
"Stack depth mismatch at offset {offset}: computed {computed_len} vs expected {expected_len}"
)));
}
for (i, (comp_type, exp_type)) in computed.stack.iter().zip(&expected.stack).enumerate() {
if !comp_type.is_assignable_to(exp_type, self.context)? {
return Err(VerifyError::VerifyError(format!(
"Stack type mismatch at offset {offset}, slot {i}: {comp_type} not assignable to {exp_type}"
)));
}
}
let min_locals = computed.locals.len().min(expected.locals.len());
for i in 0..min_locals {
let comp_type = &computed.locals[i];
let exp_type = &expected.locals[i];
if *comp_type == VerificationType::Top || *exp_type == VerificationType::Top {
continue;
}
if !comp_type.is_assignable_to(exp_type, self.context)? {
return Err(VerifyError::VerifyError(format!(
"Local type mismatch at offset {offset}, local {i}: {comp_type} not assignable to {exp_type}"
)));
}
}
Ok(())
}
fn execute_instruction(
&self,
offset: u16,
index: usize,
instruction: &Instruction,
frame: Frame,
) -> Result<(Frame, Vec<u16>, bool)> {
let mut next_frame = frame;
let next_offset = if index + 1 < self.code_info.instruction_count() {
self.code_info
.offset_at(index + 1)
.unwrap_or(self.code_info.code_length())
} else {
self.code_info.code_length()
};
let handled =
handlers::load_store::dispatch_load_store(instruction, &mut next_frame, self.context)?
|| handlers::stack::dispatch_stack(instruction, &mut next_frame)?
|| handlers::math::dispatch_math(instruction, &mut next_frame)?
|| handlers::conversion::dispatch_conversion(instruction, &mut next_frame)?
|| handlers::comparison::dispatch_comparison(instruction, &mut next_frame)?
|| handlers::control::dispatch_control(
instruction,
&mut next_frame,
self.return_type.as_ref(),
self.major_version,
self.context,
)?
|| handlers::exceptions::dispatch_exceptions(
instruction,
&mut next_frame,
self.context,
)?
|| handlers::misc::dispatch_misc(instruction, &mut next_frame, self.class_file)?
|| self.handle_reference_instruction(offset, instruction, &mut next_frame)?;
if !handled {
return Err(VerifyError::VerifyError(format!(
"Unhandled instruction at offset {offset}: {instruction:?}"
)));
}
let (successors, falls_through) =
compute_successors(offset, instruction, next_offset, &self.code_info)?;
Ok((next_frame, successors, falls_through))
}
fn handle_reference_instruction(
&self,
offset: u16,
instruction: &Instruction,
frame: &mut Frame,
) -> Result<bool> {
let resolver = ConstantPoolResolver::new(self.class_file);
match instruction {
Instruction::New(index) => {
let class_name = resolver.resolve_class(*index)?;
handlers::references::handle_new(frame, offset, &class_name)?;
}
Instruction::Newarray(atype) => {
handlers::references::handle_newarray(frame, atype)?;
}
Instruction::Anewarray(index) => {
let class_name = resolver.resolve_class(*index)?;
handlers::references::handle_anewarray(frame, &class_name)?;
}
Instruction::Multianewarray(index, dimensions) => {
let class_name = resolver.resolve_class(*index)?;
handlers::references::handle_multianewarray(frame, &class_name, *dimensions)?;
}
Instruction::Getfield(index) => {
let (class_name, _, descriptor) = resolver.resolve_field_ref(*index)?;
handlers::references::handle_getfield(
frame,
&class_name,
&descriptor,
self.context,
)?;
}
Instruction::Putfield(index) => {
let (class_name, _, descriptor) = resolver.resolve_field_ref(*index)?;
handlers::references::handle_putfield(
frame,
&class_name,
&descriptor,
self.context,
)?;
}
Instruction::Getstatic(index) => {
let (_, _, descriptor) = resolver.resolve_field_ref(*index)?;
handlers::references::handle_getstatic(frame, &descriptor)?;
}
Instruction::Putstatic(index) => {
let (_, _, descriptor) = resolver.resolve_field_ref(*index)?;
handlers::references::handle_putstatic(frame, &descriptor, self.context)?;
}
Instruction::Invokevirtual(index) | Instruction::Invokeinterface(index, _) => {
let (class_name, method_name, descriptor) = resolver.resolve_method_ref(*index)?;
handlers::references::handle_invoke(
frame,
&class_name,
&method_name,
&descriptor,
false,
self.context,
)?;
}
Instruction::Invokespecial(index) => {
let (class_name, method_name, descriptor) = resolver.resolve_method_ref(*index)?;
handlers::references::handle_invokespecial(
frame,
&class_name,
&method_name,
&descriptor,
self.context,
)?;
}
Instruction::Invokestatic(index) => {
let (class_name, method_name, descriptor) = resolver.resolve_method_ref(*index)?;
handlers::references::handle_invoke(
frame,
&class_name,
&method_name,
&descriptor,
true,
self.context,
)?;
}
Instruction::Invokedynamic(index) => {
let descriptor = resolver.resolve_invoke_dynamic(*index)?;
handlers::references::handle_invokedynamic(frame, &descriptor, self.context)?;
}
Instruction::Checkcast(index) => {
let class_name = resolver.resolve_class(*index)?;
handlers::references::handle_checkcast(frame, &class_name)?;
}
Instruction::Instanceof(_) => {
handlers::references::handle_instanceof(frame)?;
}
Instruction::Arraylength => {
handlers::references::handle_arraylength(frame)?;
}
_ => return Ok(false),
}
Ok(true)
}
fn process_exception_handlers(
&self,
offset: u16,
current_frame: &Frame,
anchor_states: &mut [Option<Frame>],
worklist: &mut Vec<usize>,
) -> Result<()> {
for handler in self.exception_table {
if handler.range_pc.contains(&offset) {
let handler_index =
self.code_info.index_at(handler.handler_pc).ok_or_else(|| {
VerifyError::VerifyError(format!(
"Invalid handler PC {}",
handler.handler_pc
))
})?;
let exception_type = if handler.catch_type == 0 {
VerificationType::java_lang_throwable()
} else {
let class_name = self
.class_file
.constant_pool
.try_get_class(handler.catch_type)
.map_err(|e| VerifyError::ClassFormatError(e.to_string()))?;
VerificationType::Object(JavaString::from(class_name))
};
let mut handler_frame =
Frame::new(self.max_locals as usize, self.max_stack as usize);
for (i, local) in current_frame.locals.iter().enumerate() {
if i < handler_frame.locals.len() {
handler_frame.locals[i] = local.clone();
}
}
handler_frame.push(exception_type)?;
if let Some(decoded) = self.stack_map_table.get(handler.handler_pc) {
let expected =
self.stack_map_table
.to_frame(decoded, self.max_locals, self.max_stack);
if expected.stack.len() != 1 {
return Err(VerifyError::VerifyError(format!(
"Exception handler at {} should have exactly one stack item",
handler.handler_pc
)));
}
anchor_states[handler_index] = Some(expected);
} else if anchor_states[handler_index].is_none() {
anchor_states[handler_index] = Some(handler_frame);
}
worklist.push(handler_index);
}
}
Ok(())
}
#[must_use]
pub fn create_diagnostic(&self, pc: u16, message: &str) -> VerificationDiagnostic {
VerificationDiagnostic::new(
&self.current_class,
&self.method_name,
&self.method_descriptor,
pc,
message,
)
}
#[must_use]
pub fn trace(&self) -> &VerificationTrace {
&self.trace
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Version;
use crate::constant::Constant;
use crate::constant_pool::ConstantPool;
struct MockContext;
impl VerificationContext for MockContext {
fn is_subclass(&self, _subclass: &str, _superclass: &str) -> Result<bool> {
Ok(false)
}
fn is_assignable(&self, _target: &str, _source: &str) -> Result<bool> {
Ok(true)
}
fn common_superclass(&self, _class1: &str, _class2: &str) -> Result<String> {
Ok("java/lang/Object".to_string())
}
}
fn create_mock_class_file() -> ClassFile<'static> {
let mut constant_pool = ConstantPool::default();
constant_pool.add(Constant::utf8("TestClass")).unwrap();
let this_class_index = constant_pool.add(Constant::Class(1)).unwrap();
constant_pool.add(Constant::utf8("testMethod")).unwrap();
constant_pool.add(Constant::utf8("()V")).unwrap();
constant_pool.add(Constant::utf8("Code")).unwrap();
ClassFile {
version: Version::Java8 { minor: 0 },
constant_pool,
access_flags: crate::ClassAccessFlags::PUBLIC,
this_class: this_class_index,
super_class: 0,
interfaces: vec![],
fields: vec![],
methods: vec![],
attributes: vec![],
code_source_url: None,
}
}
#[test]
fn test_fast_path_simple_method() {
let class_file = create_mock_class_file();
let code = vec![Instruction::Return];
let code_attribute = Attribute::Code {
name_index: 5,
max_stack: 1,
max_locals: 1,
code,
exception_table: vec![],
attributes: vec![],
};
let method = Method {
access_flags: MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC,
name_index: 3,
descriptor_index: 4,
attributes: vec![code_attribute],
};
let config = VerifierConfig::default();
let context = MockContext;
let mut verifier = FastPathVerifier::new(&class_file, &method, &context, &config).unwrap();
let result = verifier.verify();
assert!(matches!(result, FastPathResult::Success));
}
#[test]
fn test_fast_path_arithmetic() {
let class_file = create_mock_class_file();
let code = vec![
Instruction::Iconst_1,
Instruction::Iconst_2,
Instruction::Iadd,
Instruction::Pop,
Instruction::Return,
];
let code_attribute = Attribute::Code {
name_index: 5,
max_stack: 2,
max_locals: 1,
code,
exception_table: vec![],
attributes: vec![],
};
let method = Method {
access_flags: MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC,
name_index: 3,
descriptor_index: 4,
attributes: vec![code_attribute],
};
let config = VerifierConfig::default();
let context = MockContext;
let mut verifier = FastPathVerifier::new(&class_file, &method, &context, &config).unwrap();
let result = verifier.verify();
match &result {
FastPathResult::Success => {}
FastPathResult::Failed(error) => panic!("Fast path failed: {error}"),
FastPathResult::NeedsFallback(reason) => panic!("Fast path needs fallback: {reason}"),
}
}
}