From 8a2e4561f0e8d84b4271d73ebc3241de9717d186 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Fri, 17 Jul 2026 15:53:25 +0900 Subject: [PATCH 1/5] Harden JVM runtime correctness --- Cargo.lock | 2 + Cargo.toml | 5 +- classfile/src/attribute.rs | 42 +-- classfile/src/class.rs | 42 ++- classfile/src/constant_pool.rs | 113 ++++--- classfile/src/error.rs | 5 + classfile/src/field.rs | 15 +- classfile/src/interface.rs | 8 +- classfile/src/lib.rs | 2 + classfile/src/method.rs | 15 +- classfile/src/opcode.rs | 152 ++++++--- classfile/tests/test.rs | 34 +- java_runtime/src/classes/java/lang.rs | 6 +- .../classes/java/lang/class_format_error.rs | 43 +++ .../src/classes/java/lang/class_loader.rs | 14 +- java_runtime/src/classes/java/lang/object.rs | 31 +- java_runtime/src/classes/java/lang/string.rs | 136 +++++--- java_runtime/src/classes/java/lang/system.rs | 5 +- java_runtime/src/classes/java/lang/thread.rs | 76 ++--- .../java/lang/unsatisfied_link_error.rs | 43 +++ .../lang/unsupported_class_version_error.rs | 43 +++ java_runtime/src/loader.rs | 3 + java_runtime/src/runtime.rs | 1 + java_runtime/tests/classes/java/lang/mod.rs | 2 + .../tests/classes/java/lang/test_class.rs | 91 +++++- .../java/lang/test_class_initialization.rs | 238 ++++++++++++++ .../tests/classes/java/lang/test_object.rs | 133 +++++--- .../tests/classes/java/lang/test_string.rs | 73 +++++ .../tests/classes/java/lang/test_system.rs | 23 ++ jvm/src/array_class_instance.rs | 10 + jvm/src/class_instance.rs | 2 + jvm/src/class_loader.rs | 63 +++- jvm/src/jvm.rs | 138 +++++--- jvm/src/lib.rs | 2 + jvm/src/monitor.rs | 299 ++++++++++++++++++ jvm/src/type.rs | 41 ++- jvm_rust/src/array_class_instance.rs | 16 + jvm_rust/src/class_definition.rs | 127 +++++++- jvm_rust/src/class_instance.rs | 13 + jvm_rust/src/interpreter.rs | 14 +- jvm_rust/src/lib.rs | 1 + jvm_rust/src/method.rs | 9 +- src/runtime.rs | 34 +- test_data/MonitorSemantics$1.class | Bin 0 -> 196 bytes .../MonitorSemantics$CounterWorker.class | Bin 0 -> 970 bytes .../MonitorSemantics$FailingWorker.class | Bin 0 -> 732 bytes test_data/MonitorSemantics.class | Bin 0 -> 1720 bytes test_data/MonitorSemantics.txt | 1 + test_data/NativeMethod.class | Bin 0 -> 727 bytes test_data/NativeMethod.txt | 1 + test_data/src/MonitorSemantics.java | 49 +++ test_data/src/NativeMethod.java | 11 + test_utils/Cargo.toml | 1 + test_utils/src/lib.rs | 37 ++- 54 files changed, 1911 insertions(+), 354 deletions(-) create mode 100644 classfile/src/error.rs create mode 100644 java_runtime/src/classes/java/lang/class_format_error.rs create mode 100644 java_runtime/src/classes/java/lang/unsatisfied_link_error.rs create mode 100644 java_runtime/src/classes/java/lang/unsupported_class_version_error.rs create mode 100644 java_runtime/tests/classes/java/lang/test_class_initialization.rs create mode 100644 java_runtime/tests/classes/java/lang/test_system.rs create mode 100644 jvm/src/monitor.rs create mode 100644 test_data/MonitorSemantics$1.class create mode 100644 test_data/MonitorSemantics$CounterWorker.class create mode 100644 test_data/MonitorSemantics$FailingWorker.class create mode 100644 test_data/MonitorSemantics.class create mode 100644 test_data/MonitorSemantics.txt create mode 100644 test_data/NativeMethod.class create mode 100644 test_data/NativeMethod.txt create mode 100644 test_data/src/MonitorSemantics.java create mode 100644 test_data/src/NativeMethod.java diff --git a/Cargo.lock b/Cargo.lock index dc2d1196..e3b84554 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -579,6 +579,7 @@ dependencies = [ "jvm_rust", "test_utils", "tokio", + "tracing", "tracing-subscriber", ] @@ -646,6 +647,7 @@ dependencies = [ "jvm", "jvm_rust", "tokio", + "tracing", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 53459dc6..5840955a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ async-trait = { workspace = true } bytemuck = { workspace = true } anyhow = { workspace = true } +tracing = { workspace = true } tracing-subscriber = { version = "^0.3", features = ["env-filter"] } jvm = { workspace = true } @@ -53,7 +54,7 @@ java_runtime = { workspace = true } test_utils = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tokio = { workspace = true, features = ["rt-multi-thread"] } +tokio = { workspace = true, features = ["rt-multi-thread", "time"] } [target.'cfg(target_arch = "wasm32")'.dependencies] -tokio = { workspace = true, features = ["rt"] } +tokio = { workspace = true, features = ["rt", "time"] } diff --git a/classfile/src/attribute.rs b/classfile/src/attribute.rs index 645abe01..dac5a71d 100644 --- a/classfile/src/attribute.rs +++ b/classfile/src/attribute.rs @@ -19,20 +19,20 @@ pub struct CodeAttributeExceptionTable { impl CodeAttributeExceptionTable { pub fn parse<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Self> { - map((be_u16, be_u16, be_u16, be_u16), |(start_pc, end_pc, handler_pc, catch_type)| { + map_res((be_u16, be_u16, be_u16, be_u16), |(start_pc, end_pc, handler_pc, catch_type)| { let catch_type = if catch_type != 0 { - let index = constant_pool.get(&catch_type).unwrap().class_name_index(); - Some(constant_pool.get(&index).unwrap().utf8()) + let index = constant_pool.get(&catch_type).and_then(ConstantPoolItem::class_name_index).ok_or(())?; + Some(constant_pool.get(&index).and_then(ConstantPoolItem::utf8).ok_or(())?) } else { None }; - Self { + Ok::<_, ()>(Self { start_pc, end_pc, handler_pc, catch_type, - } + }) }) .parse(data) } @@ -52,7 +52,7 @@ impl AttributeInfoCode { ( be_u16, be_u16, - map(flat_map(be_u32, take), |x: &[u8]| Self::parse_code(x, constant_pool)), + map_res(flat_map(be_u32, take), |x: &[u8]| Self::parse_code(x, constant_pool)), length_count(be_u16, |x| CodeAttributeExceptionTable::parse(x, constant_pool)), length_count(be_u16, |x| AttributeInfo::parse(x, constant_pool)), ), @@ -67,22 +67,21 @@ impl AttributeInfoCode { .parse(data) } - fn parse_code(code: &[u8], constant_pool: &BTreeMap) -> BTreeMap { + fn parse_code(code: &[u8], constant_pool: &BTreeMap) -> Result, ()> { let mut result = BTreeMap::new(); let mut data = code; - loop { + while !data.is_empty() { let offset = unsafe { data.as_ptr().offset_from(code.as_ptr()) } as usize; - if let Ok((remaining, opcode)) = Opcode::parse(data, offset, constant_pool) { - result.insert(offset as _, opcode); - - data = remaining; - } else { - break; + let (remaining, opcode) = Opcode::parse(data, offset, constant_pool).map_err(|_| ())?; + if remaining.len() >= data.len() { + return Err(()); } + result.insert(offset as _, opcode); + data = remaining; } - result + Ok(result) } } @@ -114,8 +113,8 @@ impl LocalVariableTableEntry { ( be_u16, be_u16, - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), be_u16, ), |(start_pc, length, name, descriptor, index)| Self { @@ -152,7 +151,10 @@ pub enum AttributeInfo { impl AttributeInfo { pub fn parse<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Self> { map_res( - (map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), flat_map(be_u32, take)), + ( + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), + flat_map(be_u32, take), + ), |(name, info): (_, &[u8])| { Ok::<_, nom::Err<_>>(match name.as_str() { "ConstantValue" => AttributeInfo::ConstantValue(Self::parse_constant_value(info, constant_pool)?.1), @@ -180,11 +182,11 @@ impl AttributeInfo { } fn parse_source_file<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Arc> { - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()).parse(data) + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())).parse(data) } fn parse_constant_value<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], ConstantPoolReference> { - map(be_u16, |x| ConstantPoolReference::from_constant_pool(constant_pool, x as _)).parse(data) + map_res(be_u16, |x| ConstantPoolReference::from_constant_pool(constant_pool, x).ok_or(())).parse(data) } fn parse_local_variable_table<'a>( diff --git a/classfile/src/class.rs b/classfile/src/class.rs index e78a8eef..6cf78075 100644 --- a/classfile/src/class.rs +++ b/classfile/src/class.rs @@ -2,27 +2,45 @@ use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec}; use nom::{ IResult, Parser, + error::{Error, ErrorKind}, multi::length_count, number::complete::{be_u16, be_u32}, }; use java_constants::ClassAccessFlags; -use crate::{attribute::AttributeInfo, constant_pool::ConstantPoolItem, field::FieldInfo, interface::parse_interface, method::MethodInfo}; +use crate::{ + ClassFileError, attribute::AttributeInfo, constant_pool::ConstantPoolItem, field::FieldInfo, interface::parse_interface, method::MethodInfo, +}; fn parse_this_class<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Arc> { let (data, this_class) = be_u16(data)?; - let class_name_index = constant_pool.get(&this_class).unwrap().class_name_index(); + let class_name_index = constant_pool + .get(&this_class) + .and_then(ConstantPoolItem::class_name_index) + .ok_or_else(|| nom::Err::Error(Error::new(data, ErrorKind::Verify)))?; + let class_name = constant_pool + .get(&class_name_index) + .and_then(ConstantPoolItem::utf8) + .ok_or_else(|| nom::Err::Error(Error::new(data, ErrorKind::Verify)))?; - Ok((data, constant_pool.get(&class_name_index).unwrap().utf8())) + Ok((data, class_name)) } fn parse_super_class<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Option>> { let (data, super_class) = be_u16(data)?; let super_class = if super_class != 0 { - let class_name_index = constant_pool.get(&super_class).unwrap().class_name_index(); - Some(constant_pool.get(&class_name_index).unwrap().utf8()) + let class_name_index = constant_pool + .get(&super_class) + .and_then(ConstantPoolItem::class_name_index) + .ok_or_else(|| nom::Err::Error(Error::new(data, ErrorKind::Verify)))?; + Some( + constant_pool + .get(&class_name_index) + .and_then(ConstantPoolItem::utf8) + .ok_or_else(|| nom::Err::Error(Error::new(data, ErrorKind::Verify)))?, + ) } else { None }; @@ -80,12 +98,18 @@ impl ClassInfo { )) } - pub fn parse(file: &[u8]) -> Option { - let (remaining, result) = Self::parse_info(file).ok()?; + pub fn parse(file: &[u8]) -> Result { + let (remaining, result) = Self::parse_info(file).map_err(|_| ClassFileError::InvalidFormat)?; if !remaining.is_empty() { - return None; + return Err(ClassFileError::InvalidFormat); + } + if result.major_version < 45 { + return Err(ClassFileError::InvalidFormat); + } + if result.major_version > 70 { + return Err(ClassFileError::UnsupportedVersion(result.major_version)); } - Some(result) + Ok(result) } } diff --git a/classfile/src/constant_pool.rs b/classfile/src/constant_pool.rs index f97d5441..7b7e4e05 100644 --- a/classfile/src/constant_pool.rs +++ b/classfile/src/constant_pool.rs @@ -3,15 +3,14 @@ use alloc::{collections::BTreeMap, string::String, sync::Arc}; use nom::{ IResult, Parser, bytes::complete::take, + combinator::map_res, error::{Error, ErrorKind}, number::complete::{be_f32, be_f64, be_i32, be_i64, be_u16, u8}, }; fn parse_utf8(data: &[u8]) -> IResult<&[u8], Arc> { let (data, length) = be_u16(data)?; - let (data, utf8) = take(length as usize).parse(data)?; - - Ok((data, Arc::new(String::from_utf8(utf8.to_vec()).unwrap()))) + map_res(take(length as usize), |utf8: &[u8]| String::from_utf8(utf8.to_vec()).map(Arc::new)).parse(data) } #[derive(Debug)] @@ -110,6 +109,12 @@ impl ConstantPoolItem { pub fn parse_all(data: &[u8]) -> IResult<&[u8], BTreeMap> { let (remaining, count) = be_u16(data)?; + if count == 0 { + return Err(nom::Err::Error(Error::new(remaining, ErrorKind::Verify))); + } + if count == 1 { + return Ok((remaining, BTreeMap::new())); + } let mut data = remaining; let mut result = BTreeMap::new(); @@ -131,7 +136,10 @@ impl ConstantPoolItem { i += 1; } - if i >= count { + if i > count { + return Err(nom::Err::Error(Error::new(data, ErrorKind::Verify))); + } + if i == count { break; } } @@ -144,31 +152,27 @@ impl ConstantPoolItem { Self::parse_tagged(data, tag) } - pub fn utf8(&self) -> Arc { - if let ConstantPoolItem::Utf8(x) = self { - x.clone() - } else { - panic!("Invalid constant pool item"); - } + pub fn utf8(&self) -> Option> { + if let ConstantPoolItem::Utf8(x) = self { Some(x.clone()) } else { None } } - pub fn class_name_index(&self) -> u16 { + pub fn class_name_index(&self) -> Option { if let ConstantPoolItem::Class { name_index } = self { - *name_index + Some(*name_index) } else { - panic!("Invalid constant pool item"); + None } } - pub fn name_and_type(&self) -> (u16, u16) { + pub fn name_and_type(&self) -> Option<(u16, u16)> { if let ConstantPoolItem::NameAndType { name_index, descriptor_index, } = self { - (*name_index, *descriptor_index) + Some((*name_index, *descriptor_index)) } else { - panic!("Invalid constant pool item"); + None } } } @@ -187,40 +191,39 @@ pub enum ConstantPoolReference { } impl ConstantPoolReference { - pub fn from_constant_pool(constant_pool: &BTreeMap, index: u16) -> Self { - match &constant_pool.get(&index).unwrap() { - ConstantPoolItem::Integer(x) => Self::Integer(*x), - ConstantPoolItem::Float(x) => Self::Float(*x), - ConstantPoolItem::Long(x) => Self::Long(*x), - ConstantPoolItem::Double(x) => Self::Double(*x), - ConstantPoolItem::String { string_index } => Self::String(constant_pool.get(string_index).unwrap().utf8()), - ConstantPoolItem::Class { name_index } => Self::Class(constant_pool.get(name_index).unwrap().utf8()), - ConstantPoolItem::Utf8(x) => Self::String(x.clone()), + pub fn from_constant_pool(constant_pool: &BTreeMap, index: u16) -> Option { + match constant_pool.get(&index)? { + ConstantPoolItem::Integer(x) => Some(Self::Integer(*x)), + ConstantPoolItem::Float(x) => Some(Self::Float(*x)), + ConstantPoolItem::Long(x) => Some(Self::Long(*x)), + ConstantPoolItem::Double(x) => Some(Self::Double(*x)), + ConstantPoolItem::String { string_index } => Some(Self::String(constant_pool.get(string_index)?.utf8()?)), + ConstantPoolItem::Class { name_index } => Some(Self::Class(constant_pool.get(name_index)?.utf8()?)), ConstantPoolItem::Methodref { class_index, name_and_type_index, - } => Self::Method(FieldMethodref::from_reference_info( + } => Some(Self::Method(FieldMethodref::from_reference_info( constant_pool, - *class_index as _, - *name_and_type_index as _, - )), + *class_index, + *name_and_type_index, + )?)), ConstantPoolItem::Fieldref { class_index, name_and_type_index, - } => Self::Field(FieldMethodref::from_reference_info( + } => Some(Self::Field(FieldMethodref::from_reference_info( constant_pool, - *class_index as _, - *name_and_type_index as _, - )), + *class_index, + *name_and_type_index, + )?)), ConstantPoolItem::InterfaceMethodref { class_index, name_and_type_index, - } => Self::InterfaceMethodref(FieldMethodref::from_reference_info( + } => Some(Self::InterfaceMethodref(FieldMethodref::from_reference_info( constant_pool, - *class_index as _, - *name_and_type_index as _, - )), - _ => panic!("Invalid constant pool item {:?}", constant_pool.get(&index).unwrap()), + *class_index, + *name_and_type_index, + )?)), + _ => None, } } @@ -265,18 +268,36 @@ pub struct FieldMethodref { } impl FieldMethodref { - pub fn from_reference_info(constant_pool: &BTreeMap, class_index: u16, name_and_type_index: u16) -> Self { - let class_name_index = constant_pool.get(&class_index).unwrap().class_name_index(); - let class_name = constant_pool.get(&class_name_index).unwrap().utf8(); + pub fn from_reference_info(constant_pool: &BTreeMap, class_index: u16, name_and_type_index: u16) -> Option { + let class_name_index = constant_pool.get(&class_index)?.class_name_index()?; + let class_name = constant_pool.get(&class_name_index)?.utf8()?; - let (name_index, descriptor_index) = constant_pool.get(&name_and_type_index).unwrap().name_and_type(); - let name = constant_pool.get(&name_index).unwrap().utf8(); - let descriptor = constant_pool.get(&descriptor_index).unwrap().utf8(); + let (name_index, descriptor_index) = constant_pool.get(&name_and_type_index)?.name_and_type()?; + let name = constant_pool.get(&name_index)?.utf8()?; + let descriptor = constant_pool.get(&descriptor_index)?.utf8()?; - Self { + Some(Self { class: class_name, name, descriptor, - } + }) + } +} + +#[cfg(test)] +mod tests { + use super::ConstantPoolItem; + + #[test] + fn empty_constant_pool_is_valid() { + let (remaining, constant_pool) = ConstantPoolItem::parse_all(&[0x00, 0x01, 0xff]).unwrap(); + + assert!(constant_pool.is_empty()); + assert_eq!(remaining, &[0xff]); + } + + #[test] + fn long_must_fit_in_two_constant_pool_slots() { + assert!(ConstantPoolItem::parse_all(&[0x00, 0x02, 0x05, 0, 0, 0, 0, 0, 0, 0, 0]).is_err()); } } diff --git a/classfile/src/error.rs b/classfile/src/error.rs new file mode 100644 index 00000000..def88250 --- /dev/null +++ b/classfile/src/error.rs @@ -0,0 +1,5 @@ +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ClassFileError { + InvalidFormat, + UnsupportedVersion(u16), +} diff --git a/classfile/src/field.rs b/classfile/src/field.rs index ea8edebf..91489fa9 100644 --- a/classfile/src/field.rs +++ b/classfile/src/field.rs @@ -1,6 +1,11 @@ use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec}; -use nom::{IResult, Parser, combinator::map, multi::length_count, number::complete::be_u16}; +use nom::{ + IResult, Parser, + combinator::{map, map_res}, + multi::length_count, + number::complete::be_u16, +}; use java_constants::FieldAccessFlags; @@ -17,13 +22,13 @@ impl FieldInfo { pub fn parse<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Self> { map( ( - be_u16, - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), + map_res(be_u16, |x| FieldAccessFlags::from_bits(x).ok_or(())), + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), length_count(be_u16, |x| AttributeInfo::parse(x, constant_pool)), ), |(access_flags, name, descriptor, attributes)| Self { - access_flags: FieldAccessFlags::from_bits(access_flags).unwrap(), + access_flags, name, descriptor, attributes, diff --git a/classfile/src/interface.rs b/classfile/src/interface.rs index def3a034..428483f6 100644 --- a/classfile/src/interface.rs +++ b/classfile/src/interface.rs @@ -1,13 +1,13 @@ use alloc::{collections::BTreeMap, string::String, sync::Arc}; -use nom::{IResult, Parser, combinator::map, number::complete::be_u16}; +use nom::{IResult, Parser, combinator::map_res, number::complete::be_u16}; use crate::constant_pool::ConstantPoolItem; pub fn parse_interface<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Arc> { - map(be_u16, |x| { - let class_name_index = constant_pool.get(&x).unwrap().class_name_index(); - constant_pool.get(&class_name_index).unwrap().utf8() + map_res(be_u16, |x| { + let class_name_index = constant_pool.get(&x).and_then(ConstantPoolItem::class_name_index).ok_or(())?; + constant_pool.get(&class_name_index).and_then(ConstantPoolItem::utf8).ok_or(()) }) .parse(data) } diff --git a/classfile/src/lib.rs b/classfile/src/lib.rs index b1672afd..ac250fe5 100644 --- a/classfile/src/lib.rs +++ b/classfile/src/lib.rs @@ -4,6 +4,7 @@ extern crate alloc; mod attribute; mod class; mod constant_pool; +mod error; mod field; mod interface; mod method; @@ -13,6 +14,7 @@ pub use { attribute::{AttributeInfo, AttributeInfoCode}, class::ClassInfo, constant_pool::{ConstantPoolReference, FieldMethodref}, + error::ClassFileError, field::FieldInfo, method::MethodInfo, opcode::Opcode, diff --git a/classfile/src/method.rs b/classfile/src/method.rs index 770656db..620590d5 100644 --- a/classfile/src/method.rs +++ b/classfile/src/method.rs @@ -1,6 +1,11 @@ use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec}; -use nom::{IResult, Parser, combinator::map, multi::length_count, number::complete::be_u16}; +use nom::{ + IResult, Parser, + combinator::{map, map_res}, + multi::length_count, + number::complete::be_u16, +}; use java_constants::MethodAccessFlags; @@ -17,13 +22,13 @@ impl MethodInfo { pub fn parse<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Self> { map( ( - be_u16, - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), + map_res(be_u16, |x| MethodAccessFlags::from_bits(x).ok_or(())), + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), length_count(be_u16, |x| AttributeInfo::parse(x, constant_pool)), ), |(access_flags, name, descriptor, attributes)| Self { - access_flags: MethodAccessFlags::from_bits(access_flags).unwrap(), + access_flags, name, descriptor, attributes, diff --git a/classfile/src/opcode.rs b/classfile/src/opcode.rs index a57532b2..f20b8d41 100644 --- a/classfile/src/opcode.rs +++ b/classfile/src/opcode.rs @@ -3,7 +3,8 @@ use alloc::{collections::BTreeMap, vec::Vec}; use nom::{ IResult, Parser, bytes::complete::take, - combinator::{flat_map, map, success}, + combinator::{flat_map, map, map_res, success}, + error::{Error, ErrorKind}, multi::count, number::complete::{be_i16, be_i32, be_u16, i8, u8}, }; @@ -180,8 +181,9 @@ impl Opcode { 0x2b => success(Opcode::Aload(1)).parse(data), 0x2c => success(Opcode::Aload(2)).parse(data), 0x2d => success(Opcode::Aload(3)).parse(data), - 0xbd => map(be_u16, |x| { - Opcode::Anewarray(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xbd => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Class(_)) => Ok(Opcode::Anewarray(reference)), + _ => Err(()), }) .parse(data), 0xb0 => success(Opcode::Areturn).parse(data), @@ -197,8 +199,9 @@ impl Opcode { 0x10 => map(i8, Opcode::Bipush).parse(data), 0x34 => success(Opcode::Caload).parse(data), 0x55 => success(Opcode::Castore).parse(data), - 0xc0 => map(be_u16, |x| { - Opcode::Checkcast(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xc0 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Class(_)) => Ok(Opcode::Checkcast(reference)), + _ => Err(()), }) .parse(data), 0x90 => success(Opcode::D2f).parse(data), @@ -260,12 +263,14 @@ impl Opcode { 0x45 => success(Opcode::Fstore(2)).parse(data), 0x46 => success(Opcode::Fstore(3)).parse(data), 0x66 => success(Opcode::Fsub).parse(data), - 0xb4 => map(be_u16, |x| { - Opcode::Getfield(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb4 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Field(_)) => Ok(Opcode::Getfield(reference)), + _ => Err(()), }) .parse(data), - 0xb2 => map(be_u16, |x| { - Opcode::Getstatic(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb2 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Field(_)) => Ok(Opcode::Getstatic(reference)), + _ => Err(()), }) .parse(data), 0xa7 => map(be_i16, Opcode::Goto).parse(data), @@ -312,28 +317,34 @@ impl Opcode { 0x1d => success(Opcode::Iload(3)).parse(data), 0x68 => success(Opcode::Imul).parse(data), 0x74 => success(Opcode::Ineg).parse(data), - 0xc1 => map(be_u16, |x| { - Opcode::Instanceof(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xc1 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Class(_)) => Ok(Opcode::Instanceof(reference)), + _ => Err(()), }) .parse(data), - 0xba => map((be_u16, be_u16), |(x, _)| { - Opcode::Invokedynamic(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) - }) - .parse(data), - 0xb9 => map((be_u16, u8, u8), |(x, count, zero)| { - Opcode::Invokeinterface(ConstantPoolReference::from_constant_pool(constant_pool, x as _), count, zero) + 0xba => map_res((be_u16, be_u16), |_: (u16, u16)| Err::(())).parse(data), + 0xb9 => map_res((be_u16, u8, u8), |(x, count, zero)| { + match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::InterfaceMethodref(_)) if count != 0 && zero == 0 => { + Ok(Opcode::Invokeinterface(reference, count, zero)) + } + _ => Err(()), + } }) .parse(data), - 0xb7 => map(be_u16, |x| { - Opcode::Invokespecial(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb7 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Method(_)) => Ok(Opcode::Invokespecial(reference)), + _ => Err(()), }) .parse(data), - 0xb8 => map(be_u16, |x| { - Opcode::Invokestatic(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb8 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Method(_)) => Ok(Opcode::Invokestatic(reference)), + _ => Err(()), }) .parse(data), - 0xb6 => map(be_u16, |x| { - Opcode::Invokevirtual(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb6 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Method(_)) => Ok(Opcode::Invokevirtual(reference)), + _ => Err(()), }) .parse(data), 0x80 => success(Opcode::Ior).parse(data), @@ -361,10 +372,29 @@ impl Opcode { 0x94 => success(Opcode::Lcmp).parse(data), 0x09 => success(Opcode::Lconst(0)).parse(data), 0x0a => success(Opcode::Lconst(1)).parse(data), - 0x12 => map(u8, |x| Opcode::Ldc(ConstantPoolReference::from_constant_pool(constant_pool, x as _))).parse(data), - 0x13 => map(be_u16, |x| Opcode::LdcW(ConstantPoolReference::from_constant_pool(constant_pool, x as _))).parse(data), - 0x14 => map(be_u16, |x| { - Opcode::Ldc2W(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0x12 => map_res(u8, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x as u16) { + Some( + reference @ (ConstantPoolReference::Integer(_) + | ConstantPoolReference::Float(_) + | ConstantPoolReference::String(_) + | ConstantPoolReference::Class(_)), + ) => Ok(Opcode::Ldc(reference)), + _ => Err(()), + }) + .parse(data), + 0x13 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some( + reference @ (ConstantPoolReference::Integer(_) + | ConstantPoolReference::Float(_) + | ConstantPoolReference::String(_) + | ConstantPoolReference::Class(_)), + ) => Ok(Opcode::LdcW(reference)), + _ => Err(()), + }) + .parse(data), + 0x14 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ (ConstantPoolReference::Long(_) | ConstantPoolReference::Double(_))) => Ok(Opcode::Ldc2W(reference)), + _ => Err(()), }) .parse(data), 0x6d => success(Opcode::Ldiv).parse(data), @@ -376,7 +406,12 @@ impl Opcode { 0x69 => success(Opcode::Lmul).parse(data), 0x75 => success(Opcode::Lneg).parse(data), 0xab => flat_map((take((4 - (offset + 1) % 4) % 4), be_i32, be_i32), |(_, default, npairs)| { - move |x| map(count((be_i32, be_i32), npairs as _), |offsets| Opcode::Lookupswitch(default, offsets)).parse(x) + move |x| { + if npairs < 0 { + return Err(nom::Err::Error(Error::new(x, ErrorKind::Verify))); + } + map(count((be_i32, be_i32), npairs as usize), |offsets| Opcode::Lookupswitch(default, offsets)).parse(x) + } }) .parse(data), 0x81 => success(Opcode::Lor).parse(data), @@ -394,21 +429,37 @@ impl Opcode { 0x83 => success(Opcode::Lxor).parse(data), 0xc2 => success(Opcode::Monitorenter).parse(data), 0xc3 => success(Opcode::Monitorexit).parse(data), - 0xc5 => map((be_u16, u8), |(index, dimensions)| { - Opcode::Multianewarray(ConstantPoolReference::from_constant_pool(constant_pool, index as _), dimensions) + 0xc5 => map_res((be_u16, u8), |(index, dimensions)| { + match ConstantPoolReference::from_constant_pool(constant_pool, index) { + Some(reference @ ConstantPoolReference::Class(_)) if dimensions != 0 => Ok(Opcode::Multianewarray(reference, dimensions)), + _ => Err(()), + } + }) + .parse(data), + 0xbb => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Class(_)) => Ok(Opcode::New(reference)), + _ => Err(()), + }) + .parse(data), + 0xbc => map_res(u8, |array_type| { + if (4..=11).contains(&array_type) { + Ok(Opcode::Newarray(array_type)) + } else { + Err(()) + } }) .parse(data), - 0xbb => map(be_u16, |x| Opcode::New(ConstantPoolReference::from_constant_pool(constant_pool, x as _))).parse(data), - 0xbc => map(u8, Opcode::Newarray).parse(data), 0x00 => success(Opcode::Nop).parse(data), 0x57 => success(Opcode::Pop).parse(data), 0x58 => success(Opcode::Pop2).parse(data), - 0xb5 => map(be_u16, |x| { - Opcode::Putfield(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb5 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Field(_)) => Ok(Opcode::Putfield(reference)), + _ => Err(()), }) .parse(data), - 0xb3 => map(be_u16, |x| { - Opcode::Putstatic(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb3 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Field(_)) => Ok(Opcode::Putstatic(reference)), + _ => Err(()), }) .parse(data), 0xa9 => map(u8, |x| Opcode::Ret(x as u16)).parse(data), @@ -419,7 +470,13 @@ impl Opcode { 0x5f => success(Opcode::Swap).parse(data), 0xaa => flat_map((take((4 - (offset + 1) % 4) % 4), be_i32, be_i32, be_i32), |(_, default, low, high)| { move |x| { - map(count(be_i32, ((high - low) + 1) as _), |offsets| { + let Some(entry_count) = high.checked_sub(low).and_then(|range| range.checked_add(1)) else { + return Err(nom::Err::Error(Error::new(x, ErrorKind::Verify))); + }; + if entry_count < 0 { + return Err(nom::Err::Error(Error::new(x, ErrorKind::Verify))); + } + map(count(be_i32, entry_count as usize), |offsets| { Opcode::Tableswitch(default, (low..=high).zip(offsets).collect()) }) .parse(x) @@ -427,7 +484,7 @@ impl Opcode { }) .parse(data), 0xc4 => Self::parse_wide(data), - _ => panic!("Unknown opcode: {:02x}", opcode), + _ => Err(nom::Err::Error(Error::new(data, ErrorKind::Switch))), } } @@ -448,7 +505,7 @@ impl Opcode { 0x39 => map(be_u16, Opcode::Dstore).parse(data), 0x3a => map(be_u16, Opcode::Astore).parse(data), 0xa9 => map(be_u16, Opcode::Ret).parse(data), - _ => panic!("Invalid wide opcode: {:02x}", opcode), + _ => Err(nom::Err::Error(Error::new(data, ErrorKind::Switch))), } } } @@ -501,11 +558,8 @@ mod test { } #[test] - fn test_invokedynamic_consumes_reserved_bytes() { - let (remaining, opcode) = Opcode::parse(&[0xba, 0x00, 0x07, 0x00, 0x00], 0, &constant_pool()).unwrap(); - - assert!(remaining.is_empty()); - assert!(matches!(opcode, Opcode::Invokedynamic(_))); + fn test_invokedynamic_is_rejected() { + assert!(Opcode::parse(&[0xba, 0x00, 0x07, 0x00, 0x00], 0, &constant_pool()).is_err()); } #[test] @@ -525,4 +579,14 @@ mod test { assert!(matches!(opcode, Opcode::Iinc(0x012c, 1000))); assert!(remaining.is_empty()); } + + #[test] + fn test_unknown_opcode_is_rejected() { + assert!(Opcode::parse(&[0xfe], 0, &constant_pool()).is_err()); + } + + #[test] + fn test_invalid_wide_opcode_is_rejected() { + assert!(Opcode::parse(&[0xc4, 0x00], 0, &constant_pool()).is_err()); + } } diff --git a/classfile/tests/test.rs b/classfile/tests/test.rs index 77e04e51..0caba744 100644 --- a/classfile/tests/test.rs +++ b/classfile/tests/test.rs @@ -1,6 +1,6 @@ use java_constants::ClassAccessFlags; -use classfile::{AttributeInfo, ClassInfo, ConstantPoolReference, Opcode}; +use classfile::{AttributeInfo, ClassFileError, ClassInfo, ConstantPoolReference, Opcode}; #[test] fn test_hello() { @@ -132,3 +132,35 @@ fn test_invokeinterface() { panic!("Expected code attribute"); } } + +#[test] +fn test_malformed_class_files_return_structured_errors() { + let hello = include_bytes!("../../test_data/Hello.class"); + + assert_eq!(ClassInfo::parse(&[]).err(), Some(ClassFileError::InvalidFormat)); + + let mut invalid_magic = hello.to_vec(); + invalid_magic[0] = 0; + assert_eq!(ClassInfo::parse(&invalid_magic).err(), Some(ClassFileError::InvalidFormat)); + + let mut unsupported_version = hello.to_vec(); + unsupported_version[6..8].copy_from_slice(&71u16.to_be_bytes()); + assert_eq!(ClassInfo::parse(&unsupported_version).err(), Some(ClassFileError::UnsupportedVersion(71))); + + assert_eq!(ClassInfo::parse(&hello[..hello.len() / 2]).err(), Some(ClassFileError::InvalidFormat)); + + let minimal_class = vec![ + 0xca, 0xfe, 0xba, 0xbe, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x05, 0x01, 0x00, 0x04, b'T', b'e', b's', b't', 0x07, 0x00, 0x01, 0x01, 0x00, 0x10, + b'j', b'a', b'v', b'a', b'/', b'l', b'a', b'n', b'g', b'/', b'O', b'b', b'j', b'e', b'c', b't', 0x07, 0x00, 0x03, 0x00, 0x21, 0x00, 0x02, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + assert!(ClassInfo::parse(&minimal_class).is_ok()); + + let mut invalid_constant_pool_index = minimal_class.clone(); + invalid_constant_pool_index[44..46].copy_from_slice(&99u16.to_be_bytes()); + assert_eq!(ClassInfo::parse(&invalid_constant_pool_index).err(), Some(ClassFileError::InvalidFormat)); + + let mut invalid_constant_pool_type = minimal_class; + invalid_constant_pool_type[44..46].copy_from_slice(&1u16.to_be_bytes()); + assert_eq!(ClassInfo::parse(&invalid_constant_pool_type).err(), Some(ClassFileError::InvalidFormat)); +} diff --git a/java_runtime/src/classes/java/lang.rs b/java_runtime/src/classes/java/lang.rs index e41afb29..9742f853 100644 --- a/java_runtime/src/classes/java/lang.rs +++ b/java_runtime/src/classes/java/lang.rs @@ -7,6 +7,7 @@ mod byte; mod character; mod class; mod class_cast_exception; +mod class_format_error; mod class_loader; mod class_not_found_exception; mod clone_not_supported_exception; @@ -50,13 +51,15 @@ mod string_index_out_of_bounds_exception; mod system; mod thread; mod throwable; +mod unsatisfied_link_error; +mod unsupported_class_version_error; mod unsupported_operation_exception; mod virtual_machine_error; pub use self::{ abstract_method_error::AbstractMethodError, arithmetic_exception::ArithmeticException, array_index_out_of_bounds_exception::ArrayIndexOutOfBoundsException, array_store_exception::ArrayStoreException, boolean::Boolean, byte::Byte, - character::Character, class::Class, class_cast_exception::ClassCastException, class_loader::ClassLoader, + character::Character, class::Class, class_cast_exception::ClassCastException, class_format_error::ClassFormatError, class_loader::ClassLoader, class_not_found_exception::ClassNotFoundException, clone_not_supported_exception::CloneNotSupportedException, cloneable::Cloneable, comparable::Comparable, double::Double, error::Error, exception::Exception, exception_in_initializer_error::ExceptionInInitializerError, float::Float, illegal_access_exception::IllegalAccessException, illegal_argument_exception::IllegalArgumentException, @@ -69,5 +72,6 @@ pub use self::{ number_format_exception::NumberFormatException, object::Object, out_of_memory_error::OutOfMemoryError, runnable::Runnable, runtime::Runtime, runtime_exception::RuntimeException, security_exception::SecurityException, short::Short, string::String, string_buffer::StringBuffer, string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread, throwable::Throwable, + unsatisfied_link_error::UnsatisfiedLinkError, unsupported_class_version_error::UnsupportedClassVersionError, unsupported_operation_exception::UnsupportedOperationException, virtual_machine_error::VirtualMachineError, }; diff --git a/java_runtime/src/classes/java/lang/class_format_error.rs b/java_runtime/src/classes/java/lang/class_format_error.rs new file mode 100644 index 00000000..0dbd369a --- /dev/null +++ b/java_runtime/src/classes/java/lang/class_format_error.rs @@ -0,0 +1,43 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.ClassFormatError +pub struct ClassFormatError; + +impl ClassFormatError { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/ClassFormatError", + parent_class: Some("java/lang/LinkageError"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.ClassFormatError::({this:?})"); + + let _: () = jvm.invoke_special(&this, "java/lang/LinkageError", "", "()V", ()).await?; + + Ok(()) + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.ClassFormatError::({this:?}, {message:?})"); + + let _: () = jvm + .invoke_special(&this, "java/lang/LinkageError", "", "(Ljava/lang/String;)V", (message,)) + .await?; + + Ok(()) + } +} diff --git a/java_runtime/src/classes/java/lang/class_loader.rs b/java_runtime/src/classes/java/lang/class_loader.rs index e9ed3ae8..7fae556c 100644 --- a/java_runtime/src/classes/java/lang/class_loader.rs +++ b/java_runtime/src/classes/java/lang/class_loader.rs @@ -198,16 +198,14 @@ impl ClassLoader { } async fn find_class( - _: &Jvm, + jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, name: ClassInstanceRef, ) -> Result> { tracing::debug!("java.lang.ClassLoader::findClass({this:?}, {name:?})"); - // TODO raise ClassNotFoundException - - Ok(None.into()) + Err(jvm.exception("java/lang/ClassNotFoundException", "class not found").await) } async fn find_loaded_class( @@ -299,6 +297,14 @@ impl ClassLoader { ) -> Result> { tracing::debug!("java.lang.ClassLoader::defineClass({this:?}, {name:?}, {bytes:?}, {offset:?}, {length:?})"); + if bytes.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "class bytes").await); + } + let array_length = jvm.array_length(&bytes).await?; + if offset < 0 || length < 0 || (offset as usize).checked_add(length as usize).is_none_or(|end| end > array_length) { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "invalid class byte range").await); + } + let mut data = vec![0; length as usize]; jvm.array_raw_buffer(&bytes).await?.read(offset as _, &mut data)?; diff --git a/java_runtime/src/classes/java/lang/object.rs b/java_runtime/src/classes/java/lang/object.rs index 0d91b3e0..59539419 100644 --- a/java_runtime/src/classes/java/lang/object.rs +++ b/java_runtime/src/classes/java/lang/object.rs @@ -8,7 +8,7 @@ use alloc::{boxed::Box, format, vec}; use dyn_clone::clone_box; use java_class_proto::JavaMethodProto; use java_constants::MethodAccessFlags; -use jvm::{ClassInstance, ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; +use jvm::{ClassInstance, ClassInstanceRef, Jvm, MonitorWaitTimeout, Result, runtime::JavaLangString}; use crate::{Runtime, RuntimeClassProto, RuntimeContext, SpawnCallback, classes::java::lang::String}; @@ -105,13 +105,13 @@ impl Object { } async fn clone(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { - tracing::warn!("stub java.lang.Object::clone({this:?})"); + tracing::debug!("java.lang.Object::clone({this:?})"); if !jvm.is_instance(&**this, "java/lang/Cloneable") { return Err(jvm.exception("java/lang/CloneNotSupportedException", "Cannot clone this object").await); } - Ok(None.into()) + Ok(jvm.shallow_clone(&this)?.into()) } async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { @@ -131,7 +131,7 @@ impl Object { async fn notify(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.lang.Object::notify({this:?})"); - jvm.object_notify(&this, 1); + jvm.object_notify(&this, 1).await?; Ok(()) } @@ -139,7 +139,7 @@ impl Object { async fn notify_all(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.lang.Object::notifyAll({this:?})"); - jvm.object_notify(&this, usize::MAX); + jvm.object_notify(&this, usize::MAX).await?; Ok(()) } @@ -155,36 +155,39 @@ impl Object { async fn wait_long_int(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, millis: i64, nanos: i32) -> Result<()> { tracing::debug!("java.lang.Object::wait({this:?}, {millis:?}, {nanos:?})"); + if millis < 0 || !(0..=999_999).contains(&nanos) { + return Err(jvm.exception("java/lang/IllegalArgumentException", "invalid wait timeout").await); + } + struct TimeoutNotifier { - timeout: i64, - jvm: Jvm, - this: Box, + timeout: u64, + waiter: MonitorWaitTimeout, context: Box, } #[async_trait::async_trait] impl SpawnCallback for TimeoutNotifier { async fn call(&self) -> Result<()> { - self.context.sleep(Duration::from_millis(self.timeout as _)).await; - self.jvm.object_notify(&self.this, 1); // TODO this may wake an unrelated waiter + self.context.sleep(Duration::from_millis(self.timeout)).await; + self.waiter.clone().notify(); Ok(()) } } - let timeout = millis; // TODO nanos + let (waiter, timeout_notifier) = jvm.object_wait_prepare(&this).await?; + let timeout = millis as u64 + u64::from(nanos > 0); if timeout != 0 { context.spawn( jvm, Box::new(TimeoutNotifier { timeout, - jvm: jvm.clone(), - this: this.clone().into(), + waiter: timeout_notifier, context: clone_box(context), }), ); } - jvm.object_wait(&this).await?; + jvm.object_wait(waiter).await?; Ok(()) } diff --git a/java_runtime/src/classes/java/lang/string.rs b/java_runtime/src/classes/java/lang/string.rs index 6c51f0cd..3f3907fd 100644 --- a/java_runtime/src/classes/java/lang/string.rs +++ b/java_runtime/src/classes/java/lang/string.rs @@ -166,7 +166,7 @@ impl String { let bytes: Vec = jvm.load_array(&value, offset as _, count as _).await?; let charset = System::get_charset(jvm).await?; - let string = Self::decode_str(&charset, cast_slice(&bytes)); + let string = Self::decode_str(&charset, cast_slice(&bytes)).unwrap_or_else(|| RustString::from_utf8_lossy(cast_slice(&bytes)).into_owned()); let utf16 = string.encode_utf16().collect::>(); @@ -280,7 +280,7 @@ impl String { let string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; let charset = System::get_charset(jvm).await?; - let bytes = cast_vec(Self::encode_str(&charset, &string)); + let bytes = cast_vec(Self::encode_str(&charset, &string).unwrap_or_else(|| string.as_bytes().to_vec())); let mut byte_array = jvm.instantiate_array("B", bytes.len()).await?; jvm.array_raw_buffer_mut(&mut byte_array).await?.write(0, &bytes)?; @@ -413,15 +413,20 @@ impl String { async fn index_of_from(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, ch: i32, from_index: i32) -> Result { tracing::debug!("java.lang.String::indexOf({this:?}, {ch:?}, {from_index:?})"); - let this_string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; + if !(0..=u16::MAX as i32).contains(&ch) { + return Ok(-1); + } - let index = this_string - .chars() - .skip(from_index as usize) - .position(|x| x as u32 == ch as u32) - .map(|x| x as i32 + from_index); + let value = jvm.get_field(&this, "value", "[C").await?; + let length = jvm.array_length(&value).await?; + let chars: Vec = jvm.load_array(&value, 0, length).await?; + let from_index = from_index.max(0) as usize; + let index = chars + .get(from_index..) + .and_then(|chars| chars.iter().position(|&value| value == ch as u16)) + .map(|index| index + from_index); - Ok(index.unwrap_or(-1)) + Ok(index.map(|index| index as i32).unwrap_or(-1)) } async fn index_of_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, str: ClassInstanceRef) -> Result { @@ -439,30 +444,41 @@ impl String { ) -> Result { tracing::debug!("java.lang.String::indexOf({this:?}, {str:?}, {from_index})"); - let this_string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; - let str_string = JavaLangString::to_rust_string(jvm, &str.clone()).await?; + if str.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "str is null").await); + } - tracing::trace!("this_string: {this_string:?}"); - tracing::trace!("str_string: {str_string:?}"); + let value = jvm.get_field(&this, "value", "[C").await?; + let length = jvm.array_length(&value).await?; + let chars: Vec = jvm.load_array(&value, 0, length).await?; + let pattern = jvm.get_field(&str, "value", "[C").await?; + let pattern_length = jvm.array_length(&pattern).await?; + let pattern: Vec = jvm.load_array(&pattern, 0, pattern_length).await?; + let from_index = (from_index.max(0) as usize).min(chars.len()); - let chars = this_string.chars().skip(from_index as usize).collect::>(); - let str_chars = str_string.chars().collect::>(); - let index = chars.windows(str_chars.len()).position(|x| x == str_chars).map(|x| x as i32 + from_index); + if pattern.is_empty() { + return Ok(from_index as i32); + } - Ok(index.unwrap_or(-1)) + let index = chars[from_index..] + .windows(pattern.len()) + .position(|window| window == pattern) + .map(|index| index + from_index); + + Ok(index.map(|index| index as i32).unwrap_or(-1)) } async fn last_index_of(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, ch: i32) -> Result { tracing::debug!("java.lang.String::lastIndexOf({this:?}, {ch:?})"); - let this_string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; + if !(0..=u16::MAX as i32).contains(&ch) { + return Ok(-1); + } - let index = this_string - .chars() - .collect::>() // TODO i think we don't need collect.. - .into_iter() - .rposition(|x| x as u32 == ch as u32) - .map(|x| x as i32); + let value = jvm.get_field(&this, "value", "[C").await?; + let length = jvm.array_length(&value).await?; + let chars: Vec = jvm.load_array(&value, 0, length).await?; + let index = chars.iter().rposition(|&value| value == ch as u16).map(|index| index as i32); Ok(index.unwrap_or(-1)) } @@ -470,11 +486,18 @@ impl String { async fn trim(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { tracing::debug!("java.lang.String::trim({this:?})"); - let string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; - - let trimmed = string.trim().to_string(); + let value = jvm.get_field(&this, "value", "[C").await?; + let length = jvm.array_length(&value).await?; + let chars: Vec = jvm.load_array(&value, 0, length).await?; + let start = chars.iter().position(|&value| value > 0x20).unwrap_or(chars.len()); + let end = chars.iter().rposition(|&value| value > 0x20).map(|index| index + 1).unwrap_or(start); + if start == 0 && end == chars.len() { + return Ok(this); + } + let mut array = jvm.instantiate_array("C", end - start).await?; + jvm.store_array(&mut array, 0, chars[start..end].iter().copied()).await?; - Ok(JavaLangString::from_rust_string(jvm, &trimmed).await?.into()) // TODO buffer sharing + Ok(jvm.new_class("java/lang/String", "([C)V", (array,)).await?.into()) } async fn to_upper_case(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { @@ -502,14 +525,21 @@ impl String { ) -> Result { tracing::debug!("java.lang.String::startsWith({this:?}, {prefix:?}, {offset})"); - let this_string = JavaLangString::to_rust_string(jvm, &this.clone()) - .await? - .chars() - .skip(offset as usize) - .collect::(); - let prefix_string = JavaLangString::to_rust_string(jvm, &prefix.clone()).await?; + if prefix.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "prefix is null").await); + } + if offset < 0 { + return Ok(false); + } + + let value = jvm.get_field(&this, "value", "[C").await?; + let length = jvm.array_length(&value).await?; + let chars: Vec = jvm.load_array(&value, 0, length).await?; + let prefix_value = jvm.get_field(&prefix, "value", "[C").await?; + let prefix_length = jvm.array_length(&prefix_value).await?; + let prefix: Vec = jvm.load_array(&prefix_value, 0, prefix_length).await?; - Ok(this_string.starts_with(&prefix_string)) + Ok(chars.get(offset as usize..).is_some_and(|chars| chars.starts_with(&prefix))) } async fn init_empty(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { @@ -565,7 +595,9 @@ impl String { let bytes: Vec = jvm.load_array(&value, offset as _, count as _).await?; let charset = JavaLangString::to_rust_string(jvm, &charset_name).await?; - let string = Self::decode_str(&charset, cast_slice(&bytes)); + let Some(string) = Self::decode_str(&charset, cast_slice(&bytes)) else { + return Err(jvm.exception("java/io/UnsupportedEncodingException", &charset).await); + }; let utf16 = string.encode_utf16().collect::>(); @@ -605,7 +637,10 @@ impl String { let string = JavaLangString::to_rust_string(jvm, &this).await?; let charset = JavaLangString::to_rust_string(jvm, &charset_name).await?; - let bytes = cast_vec(Self::encode_str(&charset, &string)); + let Some(bytes) = Self::encode_str(&charset, &string) else { + return Err(jvm.exception("java/io/UnsupportedEncodingException", &charset).await); + }; + let bytes = cast_vec(bytes); let mut byte_array = jvm.instantiate_array("B", bytes.len()).await?; jvm.array_raw_buffer_mut(&mut byte_array).await?.write(0, &bytes)?; @@ -700,11 +735,16 @@ impl String { return Ok(-1); } - let this_string = JavaLangString::to_rust_string(jvm, &this).await?; - let chars: Vec = this_string.chars().collect(); + if !(0..=u16::MAX as i32).contains(&ch) { + return Ok(-1); + } + + let value = jvm.get_field(&this, "value", "[C").await?; + let length = jvm.array_length(&value).await?; + let chars: Vec = jvm.load_array(&value, 0, length).await?; let end = (from_index as usize + 1).min(chars.len()); - let index = chars[..end].iter().rposition(|&c| c as u32 == ch as u32).map(|x| x as i32); + let index = chars[..end].iter().rposition(|&value| value == ch as u16).map(|index| index as i32); Ok(index.unwrap_or(-1)) } @@ -781,22 +821,22 @@ impl String { Ok(new_string.into()) } - fn decode_str(charset: &str, bytes: &[u8]) -> RustString { - match charset.to_ascii_uppercase().replace('_', "-").as_str() { + fn decode_str(charset: &str, bytes: &[u8]) -> Option { + Some(match charset.to_ascii_uppercase().replace('_', "-").as_str() { "UTF-8" | "UTF8" => RustString::from_utf8_lossy(bytes).into_owned(), "EUC-KR" | "EUCKR" | "KS-C-5601-1987" | "MS949" | "CP949" => encoding_rs::EUC_KR.decode(bytes).0.to_string(), "ISO-8859-1" | "LATIN1" | "US-ASCII" | "ASCII" => bytes.iter().map(|&b| b as char).collect(), - _ => unimplemented!("unsupported charset: {}", charset), - } + _ => return None, + }) } - fn encode_str(charset: &str, string: &str) -> Vec { - match charset.to_ascii_uppercase().replace('_', "-").as_str() { + fn encode_str(charset: &str, string: &str) -> Option> { + Some(match charset.to_ascii_uppercase().replace('_', "-").as_str() { "UTF-8" | "UTF8" => string.as_bytes().to_vec(), "EUC-KR" | "EUCKR" | "KS-C-5601-1987" | "MS949" | "CP949" => encoding_rs::EUC_KR.encode(string).0.to_vec(), "ISO-8859-1" | "LATIN1" => string.chars().map(|c| if (c as u32) <= 0xff { c as u8 } else { b'?' }).collect(), "US-ASCII" | "ASCII" => string.chars().map(|c| if c.is_ascii() { c as u8 } else { b'?' }).collect(), - _ => unimplemented!("unsupported charset: {}", charset), - } + _ => return None, + }) } } diff --git a/java_runtime/src/classes/java/lang/system.rs b/java_runtime/src/classes/java/lang/system.rs index 6b9f8451..dcdfe7c7 100644 --- a/java_runtime/src/classes/java/lang/system.rs +++ b/java_runtime/src/classes/java/lang/system.rs @@ -152,9 +152,10 @@ impl System { Ok(value) } - async fn exit(_jvm: &Jvm, _: &mut RuntimeContext, status: i32) -> Result<()> { - tracing::warn!("stub java.lang.System::exit({status})"); + async fn exit(_jvm: &Jvm, context: &mut RuntimeContext, status: i32) -> Result<()> { + tracing::debug!("java.lang.System::exit({status})"); + context.exit(status); Ok(()) } diff --git a/java_runtime/src/classes/java/lang/thread.rs b/java_runtime/src/classes/java/lang/thread.rs index ecdc5469..91e2ea84 100644 --- a/java_runtime/src/classes/java/lang/thread.rs +++ b/java_runtime/src/classes/java/lang/thread.rs @@ -30,8 +30,8 @@ impl Thread { Self::init_with_runnable_and_name, Default::default(), ), - JavaMethodProto::new("start", "()V", Self::start, Default::default()), - JavaMethodProto::new("join", "()V", Self::join, Default::default()), + JavaMethodProto::new("start", "()V", Self::start, MethodAccessFlags::SYNCHRONIZED), + JavaMethodProto::new("join", "()V", Self::join, MethodAccessFlags::SYNCHRONIZED), JavaMethodProto::new("run", "()V", Self::run, Default::default()), JavaMethodProto::new("isAlive", "()Z", Self::is_alive, Default::default()), JavaMethodProto::new("getName", "()Ljava/lang/String;", Self::get_name, Default::default()), @@ -204,40 +204,45 @@ impl Thread { let result: Result<()> = self.jvm.invoke_virtual(&self.this, "run", "()V", []).await; - if let Err(jvm::JavaError::JavaException(x)) = result { - let string_writer = self.jvm.new_class("java/io/StringWriter", "()V", ()).await.unwrap(); - let print_writer = self - .jvm - .new_class("java/io/PrintWriter", "(Ljava/io/Writer;)V", (string_writer.clone(),)) - .await - .unwrap(); - - let _: () = self - .jvm - .invoke_virtual(&x, "printStackTrace", "(Ljava/io/PrintWriter;)V", (print_writer,)) - .await - .unwrap(); - - let trace = self - .jvm - .invoke_virtual(&string_writer, "toString", "()Ljava/lang/String;", []) - .await - .unwrap(); - - tracing::error!( - "Uncaught exception in thread {}:\n{}", - self.thread_id, - JavaLangString::to_rust_string(&self.jvm, &trace).await.unwrap() - ); - } else { - result?; + if let Err(jvm::JavaError::JavaException(exception)) = &result { + let trace = async { + let string_writer = self.jvm.new_class("java/io/StringWriter", "()V", ()).await?; + let print_writer = self + .jvm + .new_class("java/io/PrintWriter", "(Ljava/io/Writer;)V", (string_writer.clone(),)) + .await?; + let _: () = self + .jvm + .invoke_virtual(exception, "printStackTrace", "(Ljava/io/PrintWriter;)V", (print_writer,)) + .await?; + let trace = self.jvm.invoke_virtual(&string_writer, "toString", "()Ljava/lang/String;", []).await?; + JavaLangString::to_rust_string(&self.jvm, &trace).await + } + .await; + + match trace { + Ok(trace) => tracing::error!("Uncaught exception in thread {}:\n{}", self.thread_id, trace), + Err(error) => tracing::error!(?error, "failed to format uncaught exception in thread {}", self.thread_id), + } } - self.jvm.detach_thread()?; - let mut this = self.this.clone(); - self.jvm.put_field(&mut this, "alive", "Z", false).await.unwrap(); - self.jvm.object_notify(&self.this, usize::MAX); + let cleanup = if let Err(error) = self.jvm.monitor_enter(&self.this).await { + Err(error) + } else { + let alive_result = self.jvm.put_field(&mut this, "alive", "Z", false).await; + let notify_result = if alive_result.is_ok() { + self.jvm.object_notify(&self.this, usize::MAX).await + } else { + Ok(()) + }; + let exit_result = self.jvm.monitor_exit(&self.this).await; + alive_result.and(notify_result).and(exit_result) + }; + let detach_result = self.jvm.detach_thread(); + + cleanup?; + detach_result?; Ok(()) } @@ -271,16 +276,15 @@ impl Thread { Ok(()) } - async fn join(jvm: &Jvm, _context: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + async fn join(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.lang.Thread::join({this:?})"); loop { - let listener = jvm.object_listen(&this); let alive: bool = jvm.get_field(&this, "alive", "Z").await?; if !alive { return Ok(()); } - listener.await; + let _: () = jvm.invoke_virtual(&this, "wait", "()V", ()).await?; } } diff --git a/java_runtime/src/classes/java/lang/unsatisfied_link_error.rs b/java_runtime/src/classes/java/lang/unsatisfied_link_error.rs new file mode 100644 index 00000000..9650a54c --- /dev/null +++ b/java_runtime/src/classes/java/lang/unsatisfied_link_error.rs @@ -0,0 +1,43 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.UnsatisfiedLinkError +pub struct UnsatisfiedLinkError; + +impl UnsatisfiedLinkError { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/UnsatisfiedLinkError", + parent_class: Some("java/lang/LinkageError"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.UnsatisfiedLinkError::({this:?})"); + + let _: () = jvm.invoke_special(&this, "java/lang/LinkageError", "", "()V", ()).await?; + + Ok(()) + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.UnsatisfiedLinkError::({this:?}, {message:?})"); + + let _: () = jvm + .invoke_special(&this, "java/lang/LinkageError", "", "(Ljava/lang/String;)V", (message,)) + .await?; + + Ok(()) + } +} diff --git a/java_runtime/src/classes/java/lang/unsupported_class_version_error.rs b/java_runtime/src/classes/java/lang/unsupported_class_version_error.rs new file mode 100644 index 00000000..18c354c1 --- /dev/null +++ b/java_runtime/src/classes/java/lang/unsupported_class_version_error.rs @@ -0,0 +1,43 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.UnsupportedClassVersionError +pub struct UnsupportedClassVersionError; + +impl UnsupportedClassVersionError { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/UnsupportedClassVersionError", + parent_class: Some("java/lang/ClassFormatError"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.UnsupportedClassVersionError::({this:?})"); + + let _: () = jvm.invoke_special(&this, "java/lang/ClassFormatError", "", "()V", ()).await?; + + Ok(()) + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.UnsupportedClassVersionError::({this:?}, {message:?})"); + + let _: () = jvm + .invoke_special(&this, "java/lang/ClassFormatError", "", "(Ljava/lang/String;)V", (message,)) + .await?; + + Ok(()) + } +} diff --git a/java_runtime/src/loader.rs b/java_runtime/src/loader.rs index 82ca5170..be9a329d 100644 --- a/java_runtime/src/loader.rs +++ b/java_runtime/src/loader.rs @@ -43,6 +43,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::Boolean::as_proto(), crate::classes::java::lang::Class::as_proto(), crate::classes::java::lang::ClassCastException::as_proto(), + crate::classes::java::lang::ClassFormatError::as_proto(), crate::classes::java::lang::ClassLoader::as_proto(), crate::classes::java::lang::ClassNotFoundException::as_proto(), crate::classes::java::lang::Byte::as_proto(), @@ -89,6 +90,8 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::Thread::as_proto(), crate::classes::java::lang::Throwable::as_proto(), crate::classes::java::lang::UnsupportedOperationException::as_proto(), + crate::classes::java::lang::UnsupportedClassVersionError::as_proto(), + crate::classes::java::lang::UnsatisfiedLinkError::as_proto(), crate::classes::java::lang::VirtualMachineError::as_proto(), crate::classes::java::net::JarURLConnection::as_proto(), crate::classes::java::net::MalformedURLException::as_proto(), diff --git a/java_runtime/src/runtime.rs b/java_runtime/src/runtime.rs index ae12d782..d653d008 100644 --- a/java_runtime/src/runtime.rs +++ b/java_runtime/src/runtime.rs @@ -19,6 +19,7 @@ pub trait Runtime: Sync + Send + DynClone { async fn sleep(&self, duration: Duration); async fn r#yield(&self); fn spawn(&self, jvm: &Jvm, callback: Box); + fn exit(&self, status: i32); fn now(&self) -> u64; // unix time in millis fn current_task_id(&self) -> u64; diff --git a/java_runtime/tests/classes/java/lang/mod.rs b/java_runtime/tests/classes/java/lang/mod.rs index b18634d2..ced07ebc 100644 --- a/java_runtime/tests/classes/java/lang/mod.rs +++ b/java_runtime/tests/classes/java/lang/mod.rs @@ -2,6 +2,7 @@ mod test_boolean; mod test_byte; mod test_character; mod test_class; +mod test_class_initialization; mod test_cldc11_exceptions; mod test_double; mod test_float; @@ -13,5 +14,6 @@ mod test_object; mod test_short; mod test_string; mod test_string_buffer; +mod test_system; mod test_thread; mod test_throwable; diff --git a/java_runtime/tests/classes/java/lang/test_class.rs b/java_runtime/tests/classes/java/lang/test_class.rs index edcb6f9a..ae829c4d 100644 --- a/java_runtime/tests/classes/java/lang/test_class.rs +++ b/java_runtime/tests/classes/java/lang/test_class.rs @@ -1,6 +1,6 @@ -use java_runtime::classes::java::lang::{Class, String}; +use java_runtime::classes::java::lang::{Class, ClassLoader, String}; use jvm::{ - ClassInstanceRef, JavaError, Result, + Array, ClassInstanceRef, JavaError, Result, runtime::{JavaLangClass, JavaLangString}, }; @@ -244,3 +244,90 @@ async fn test_cldc_class_queries_and_new_instance() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_base_class_loader_find_class_throws() -> Result<()> { + let jvm = test_jvm().await?; + let loader = jvm.new_class("java/lang/ClassLoader", "(Ljava/lang/ClassLoader;)V", (None,)).await?; + let name = JavaLangString::from_rust_string(&jvm, "missing.Type").await?; + + let result: Result> = jvm + .invoke_virtual(&loader, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;", (name,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("ClassLoader.findClass must throw ClassNotFoundException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/ClassNotFoundException")); + + Ok(()) +} + +#[tokio::test] +async fn test_define_class_translates_parser_errors_to_java_errors() -> Result<()> { + let jvm = test_jvm().await?; + let loader: ClassInstanceRef = jvm + .new_class("java/lang/ClassLoader", "(Ljava/lang/ClassLoader;)V", (None,)) + .await? + .into(); + let name: ClassInstanceRef = None.into(); + + let mut unsupported_version = include_bytes!("../../../../../test_data/Hello.class").to_vec(); + unsupported_version[6..8].copy_from_slice(&71u16.to_be_bytes()); + + for (data, expected_exception) in [ + (vec![0, 1, 2, 3], "java/lang/ClassFormatError"), + (unsupported_version, "java/lang/UnsupportedClassVersionError"), + ] { + let length = data.len() as i32; + let mut bytes = jvm.instantiate_array("B", data.len()).await?; + jvm.store_array(&mut bytes, 0, data.into_iter().map(|byte| byte as i8).collect::>()) + .await?; + + let result: Result> = jvm + .invoke_virtual( + &loader, + "defineClass", + "(Ljava/lang/String;[BII)Ljava/lang/Class;", + (name.clone(), bytes, 0, length), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("ClassLoader.defineClass must translate malformed class files to Java errors"); + }; + assert!(jvm.is_instance(&*exception, expected_exception)); + } + + Ok(()) +} + +#[tokio::test] +async fn test_define_class_validates_the_byte_range() -> Result<()> { + let jvm = test_jvm().await?; + let loader: ClassInstanceRef = jvm + .new_class("java/lang/ClassLoader", "(Ljava/lang/ClassLoader;)V", (None,)) + .await? + .into(); + let name: ClassInstanceRef = None.into(); + let bytes: ClassInstanceRef> = jvm.instantiate_array("B", 4).await?.into(); + + for (bytes, offset, length, expected_exception) in [ + (bytes.clone(), -1, 1, "java/lang/IndexOutOfBoundsException"), + (bytes, 2, 3, "java/lang/IndexOutOfBoundsException"), + (ClassInstanceRef::new(None), 0, 0, "java/lang/NullPointerException"), + ] { + let result: Result> = jvm + .invoke_virtual( + &loader, + "defineClass", + "(Ljava/lang/String;[BII)Ljava/lang/Class;", + (name.clone(), bytes, offset, length), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("ClassLoader.defineClass must validate its byte range"); + }; + assert!(jvm.is_instance(&*exception, expected_exception)); + } + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_class_initialization.rs b/java_runtime/tests/classes/java/lang/test_class_initialization.rs new file mode 100644 index 00000000..206cbe2a --- /dev/null +++ b/java_runtime/tests/classes/java/lang/test_class_initialization.rs @@ -0,0 +1,238 @@ +use alloc::{boxed::Box, collections::BTreeMap, sync::Arc, vec}; +use core::{ + sync::atomic::{AtomicUsize, Ordering}, + time::Duration, +}; +use std::sync::Mutex; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{FieldAccessFlags, MethodAccessFlags}; +use java_runtime::{Runtime, RuntimeClassProto, RuntimeContext, SpawnCallback}; +use jvm::{JavaError, Jvm, Result}; +use jvm_rust::ClassDefinitionImpl; +use test_utils::{TestRuntime, create_test_jvm}; + +struct ConcurrentInitialization; + +static FAILING_INITIALIZATION_CALLS: AtomicUsize = AtomicUsize::new(0); + +impl ConcurrentInitialization { + fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "ConcurrentInitialization", + parent_class: Some("java/lang/Object"), + interfaces: vec![], + methods: vec![JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC)], + fields: vec![ + JavaFieldProto::new("count", "I", FieldAccessFlags::STATIC), + JavaFieldProto::new("value", "I", FieldAccessFlags::STATIC), + ], + access_flags: Default::default(), + } + } + + async fn clinit(jvm: &Jvm, context: &mut RuntimeContext) -> Result<()> { + let count: i32 = jvm.get_static_field("ConcurrentInitialization", "count", "I").await?; + jvm.put_static_field("ConcurrentInitialization", "count", "I", count + 1).await?; + context.sleep(Duration::from_millis(50)).await; + jvm.put_static_field("ConcurrentInitialization", "value", "I", 42i32).await + } +} + +struct RecursiveInitialization; + +impl RecursiveInitialization { + fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "RecursiveInitialization", + parent_class: Some("java/lang/Object"), + interfaces: vec![], + methods: vec![JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC)], + fields: vec![ + JavaFieldProto::new("count", "I", FieldAccessFlags::STATIC), + JavaFieldProto::new("value", "I", FieldAccessFlags::STATIC), + ], + access_flags: Default::default(), + } + } + + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + let count: i32 = jvm.get_static_field("RecursiveInitialization", "count", "I").await?; + jvm.put_static_field("RecursiveInitialization", "count", "I", count + 1).await?; + jvm.put_static_field("RecursiveInitialization", "value", "I", 7i32).await + } +} + +struct FailingInitialization; + +impl FailingInitialization { + fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "FailingInitialization", + parent_class: Some("java/lang/Object"), + interfaces: vec![], + methods: vec![JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC)], + fields: vec![JavaFieldProto::new("value", "I", FieldAccessFlags::STATIC)], + access_flags: Default::default(), + } + } + + async fn clinit(jvm: &Jvm, context: &mut RuntimeContext) -> Result<()> { + FAILING_INITIALIZATION_CALLS.fetch_add(1, Ordering::SeqCst); + context.sleep(Duration::from_millis(50)).await; + Err(jvm.exception("java/lang/IllegalArgumentException", "initialization failed").await) + } +} + +#[tokio::test] +async fn class_initialization_waits_for_the_owner_thread() -> Result<()> { + let runtime = TestRuntime::new(BTreeMap::new()); + let jvm = create_test_jvm(runtime.clone()).await?; + jvm.register_class( + Box::new(ClassDefinitionImpl::from_class_proto( + ConcurrentInitialization::as_proto(), + Box::new(runtime.clone()) as Box<_>, + )), + None, + ) + .await?; + + struct ReadValue { + jvm: Jvm, + completed: Arc, + failures: Arc, + values: Arc>>, + } + + #[async_trait::async_trait] + impl SpawnCallback for ReadValue { + async fn call(&self) -> Result<()> { + self.jvm.attach_thread(None).await?; + match self.jvm.get_static_field("ConcurrentInitialization", "value", "I").await { + Ok(value) => self.values.lock().unwrap().push(value), + Err(_) => { + self.failures.fetch_add(1, Ordering::SeqCst); + } + } + self.jvm.detach_thread()?; + self.completed.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + } + + let completed = Arc::new(AtomicUsize::new(0)); + let failures = Arc::new(AtomicUsize::new(0)); + let values = Arc::new(Mutex::new(alloc::vec::Vec::new())); + for _ in 0..2 { + runtime.spawn( + &jvm, + Box::new(ReadValue { + jvm: jvm.clone(), + completed: completed.clone(), + failures: failures.clone(), + values: values.clone(), + }), + ); + } + + for _ in 0..100 { + if completed.load(Ordering::SeqCst) == 2 { + break; + } + runtime.sleep(Duration::from_millis(5)).await; + } + + assert_eq!(completed.load(Ordering::SeqCst), 2); + assert_eq!(failures.load(Ordering::SeqCst), 0); + assert_eq!(*values.lock().unwrap(), [42, 42]); + assert_eq!(jvm.get_static_field::("ConcurrentInitialization", "count", "I").await?, 1); + + Ok(()) +} + +#[tokio::test] +async fn class_initialization_allows_same_thread_recursion() -> Result<()> { + let runtime = TestRuntime::new(BTreeMap::new()); + let jvm = create_test_jvm(runtime.clone()).await?; + jvm.register_class( + Box::new(ClassDefinitionImpl::from_class_proto( + RecursiveInitialization::as_proto(), + Box::new(runtime) as Box<_>, + )), + None, + ) + .await?; + + assert_eq!(jvm.get_static_field::("RecursiveInitialization", "value", "I").await?, 7); + assert_eq!(jvm.get_static_field::("RecursiveInitialization", "count", "I").await?, 1); + + Ok(()) +} + +#[tokio::test] +async fn failed_class_initialization_wakes_waiters_and_becomes_erroneous() -> Result<()> { + FAILING_INITIALIZATION_CALLS.store(0, Ordering::SeqCst); + let runtime = TestRuntime::new(BTreeMap::new()); + let jvm = create_test_jvm(runtime.clone()).await?; + jvm.register_class( + Box::new(ClassDefinitionImpl::from_class_proto( + FailingInitialization::as_proto(), + Box::new(runtime.clone()) as Box<_>, + )), + None, + ) + .await?; + + struct ReadFailingValue { + jvm: Jvm, + completed: Arc, + errors: Arc>>, + } + + #[async_trait::async_trait] + impl SpawnCallback for ReadFailingValue { + async fn call(&self) -> Result<()> { + self.jvm.attach_thread(None).await?; + if let Err(JavaError::JavaException(exception)) = self.jvm.get_static_field::("FailingInitialization", "value", "I").await { + self.errors.lock().unwrap().push(exception.class_definition().name()); + } + self.jvm.detach_thread()?; + self.completed.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + } + + let completed = Arc::new(AtomicUsize::new(0)); + let errors = Arc::new(Mutex::new(alloc::vec::Vec::new())); + for _ in 0..2 { + runtime.spawn( + &jvm, + Box::new(ReadFailingValue { + jvm: jvm.clone(), + completed: completed.clone(), + errors: errors.clone(), + }), + ); + } + + for _ in 0..100 { + if completed.load(Ordering::SeqCst) == 2 { + break; + } + runtime.sleep(Duration::from_millis(5)).await; + } + + let mut errors = errors.lock().unwrap().clone(); + errors.sort(); + assert_eq!(completed.load(Ordering::SeqCst), 2); + assert_eq!(FAILING_INITIALIZATION_CALLS.load(Ordering::SeqCst), 1); + assert_eq!(errors, ["java/lang/ExceptionInInitializerError", "java/lang/NoClassDefFoundError"]); + + let result = jvm.get_static_field::("FailingInitialization", "value", "I").await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("an erroneous class must remain erroneous"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NoClassDefFoundError")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_object.rs b/java_runtime/tests/classes/java/lang/test_object.rs index ef3c7f33..0ed60931 100644 --- a/java_runtime/tests/classes/java/lang/test_object.rs +++ b/java_runtime/tests/classes/java/lang/test_object.rs @@ -4,13 +4,33 @@ use core::{ time::Duration, }; -use alloc::{boxed::Box, collections::btree_map::BTreeMap, sync::Arc}; +use alloc::{boxed::Box, collections::btree_map::BTreeMap, sync::Arc, vec}; -use java_runtime::{Runtime, SpawnCallback, classes::java::lang::Object}; -use jvm::{ClassInstanceRef, JavaError, Jvm, Result}; +use java_class_proto::JavaFieldProto; +use java_runtime::{Runtime, RuntimeClassProto, SpawnCallback, classes::java::lang::Object}; +use jvm::{Array, ClassInstanceRef, JavaError, Jvm, Result}; +use jvm_rust::ClassDefinitionImpl; use test_utils::{TestRuntime, create_test_jvm}; +struct CloneableObject; + +impl CloneableObject { + fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "CloneableObject", + parent_class: Some("java/lang/Object"), + interfaces: vec!["java/lang/Cloneable"], + methods: vec![], + fields: vec![ + JavaFieldProto::new("value", "I", Default::default()), + JavaFieldProto::new("reference", "Ljava/lang/Object;", Default::default()), + ], + access_flags: Default::default(), + } + } +} + #[tokio::test] async fn test_wait() -> Result<()> { let runtime = TestRuntime::new(BTreeMap::new()); @@ -34,7 +54,9 @@ async fn test_wait() -> Result<()> { self.runtime.sleep(Duration::from_millis(100)).await; self.notified.store(true, Ordering::Relaxed); + self.jvm.monitor_enter(&self.target).await?; let _: () = self.jvm.invoke_virtual(&self.target, "notify", "()V", ()).await?; + self.jvm.monitor_exit(&self.target).await?; self.jvm.detach_thread()?; @@ -53,7 +75,9 @@ async fn test_wait() -> Result<()> { ); assert!(!notified.load(Ordering::Relaxed)); + jvm.monitor_enter(&object).await?; let _: () = jvm.invoke_virtual(&object, "wait", "()V", ()).await?; + jvm.monitor_exit(&object).await?; assert!(notified.load(Ordering::Relaxed)); Ok(()) @@ -63,46 +87,30 @@ async fn test_wait_timeout() -> Result<()> { let runtime = TestRuntime::new(BTreeMap::new()); let jvm = create_test_jvm(runtime.clone()).await?; - let notified = Arc::new(AtomicBool::new(false)); - let object = jvm.new_class("java/lang/Object", "()V", ()).await?; + jvm.monitor_enter(&object).await?; + let _: () = jvm.invoke_virtual(&object, "wait", "(J)V", (100i64,)).await?; + jvm.monitor_exit(&object).await?; - struct Notifier { - jvm: Jvm, - notified: Arc, - runtime: TestRuntime, - target: ClassInstanceRef, - } - - #[async_trait::async_trait] - impl SpawnCallback for Notifier { - async fn call(&self) -> Result<()> { - self.jvm.attach_thread(None).await?; - - self.runtime.sleep(Duration::from_millis(1000)).await; - self.notified.store(true, Ordering::Relaxed); - let _: () = self.jvm.invoke_virtual(&self.target, "notify", "()V", ()).await?; + Ok(()) +} - self.jvm.detach_thread()?; +#[tokio::test] +async fn test_wait_and_notify_require_monitor_ownership() -> Result<()> { + let runtime = TestRuntime::new(BTreeMap::new()); + let jvm = create_test_jvm(runtime).await?; + let object = jvm.new_class("java/lang/Object", "()V", ()).await?; - Ok(()) - } + for result in [ + jvm.invoke_virtual::<_, ()>(&object, "notify", "()V", ()).await, + jvm.invoke_virtual::<_, ()>(&object, "wait", "(J)V", (1i64,)).await, + ] { + let Err(JavaError::JavaException(exception)) = result else { + panic!("monitor ownership violation must throw IllegalMonitorStateException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalMonitorStateException")); } - runtime.spawn( - &jvm, - Box::new(Notifier { - jvm: jvm.clone(), - notified: notified.clone(), - runtime: runtime.clone(), - target: object.clone().into(), - }), - ); - - assert!(!notified.load(Ordering::Relaxed)); - let _: () = jvm.invoke_virtual(&object, "wait", "(J)V", (100i64,)).await?; - assert!(!notified.load(Ordering::Relaxed)); - Ok(()) } @@ -124,6 +132,57 @@ async fn test_clone_not_cloneable() -> Result<()> { Ok(()) } +#[tokio::test] +async fn test_clone_creates_shallow_object_and_array_copies() -> Result<()> { + let runtime = TestRuntime::new(BTreeMap::new()); + let jvm = create_test_jvm(runtime.clone()).await?; + jvm.register_class( + Box::new(ClassDefinitionImpl::from_class_proto( + CloneableObject::as_proto(), + Box::new(runtime) as Box<_>, + )), + None, + ) + .await?; + + let mut original = jvm.instantiate_class("CloneableObject").await?; + let reference = jvm.new_class("java/lang/Object", "()V", ()).await?; + jvm.put_field(&mut original, "value", "I", 7i32).await?; + jvm.put_field(&mut original, "reference", "Ljava/lang/Object;", reference.clone()).await?; + + let mut cloned: ClassInstanceRef = jvm.invoke_virtual(&original, "clone", "()Ljava/lang/Object;", ()).await?; + assert_ne!(original.identity(), cloned.identity()); + assert_eq!(jvm.get_field::(&cloned, "value", "I").await?, 7); + let cloned_reference = jvm + .get_field::>(&cloned, "reference", "Ljava/lang/Object;") + .await?; + assert_eq!(reference.identity(), cloned_reference.identity()); + + jvm.put_field(&mut cloned, "value", "I", 9i32).await?; + assert_eq!(jvm.get_field::(&original, "value", "I").await?, 7); + + let mut array = jvm.instantiate_array("I", 2).await?; + jvm.store_array(&mut array, 0, [1i32, 2i32]).await?; + let mut cloned_array: ClassInstanceRef> = jvm.invoke_virtual(&array, "clone", "()Ljava/lang/Object;", ()).await?; + assert_ne!(array.identity(), cloned_array.identity()); + jvm.store_array(&mut cloned_array, 0, [9i32]).await?; + assert_eq!(jvm.load_array::(&array, 0, 2).await?, [1, 2]); + assert_eq!(jvm.load_array::(&cloned_array, 0, 2).await?, [9, 2]); + + let mut reference_array = jvm.instantiate_array("Ljava/lang/Object;", 1).await?; + jvm.store_array(&mut reference_array, 0, [reference.clone()]).await?; + let mut cloned_reference_array: ClassInstanceRef> = + jvm.invoke_virtual(&reference_array, "clone", "()Ljava/lang/Object;", ()).await?; + let cloned_element = jvm.load_array::>(&cloned_reference_array, 0, 1).await?; + assert_eq!(cloned_element[0].identity(), reference.identity()); + let replacement: ClassInstanceRef = jvm.new_class("java/lang/Object", "()V", ()).await?.into(); + jvm.store_array(&mut cloned_reference_array, 0, [replacement]).await?; + let original_element = jvm.load_array::>(&reference_array, 0, 1).await?; + assert_eq!(original_element[0].identity(), reference.identity()); + + Ok(()) +} + #[tokio::test] async fn test_hash_code_is_stable_for_same_object() -> Result<()> { let runtime = TestRuntime::new(BTreeMap::new()); diff --git a/java_runtime/tests/classes/java/lang/test_string.rs b/java_runtime/tests/classes/java/lang/test_string.rs index 8289b91b..a87bb6ff 100644 --- a/java_runtime/tests/classes/java/lang/test_string.rs +++ b/java_runtime/tests/classes/java/lang/test_string.rs @@ -413,3 +413,76 @@ async fn test_get_bytes_ascii_replaces_non_ascii() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_index_of_uses_utf16_indices_and_handles_empty_patterns() -> Result<()> { + let jvm = test_jvm().await?; + let string = JavaLangString::from_rust_string(&jvm, "a😀b").await?; + let empty = JavaLangString::from_rust_string(&jvm, "").await?; + let emoji = JavaLangString::from_rust_string(&jvm, "😀").await?; + let tail = JavaLangString::from_rust_string(&jvm, "b").await?; + + assert_eq!( + jvm.invoke_virtual::<_, i32>(&string, "indexOf", "(Ljava/lang/String;)I", (emoji,)) + .await?, + 1 + ); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&string, "indexOf", "(Ljava/lang/String;)I", (tail,)).await?, + 3 + ); + assert_eq!(jvm.invoke_virtual::<_, i32>(&string, "indexOf", "(II)I", (b'b' as i32, -10)).await?, 3); + assert_eq!(jvm.invoke_virtual::<_, i32>(&string, "lastIndexOf", "(I)I", (b'b' as i32,)).await?, 3); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&string, "indexOf", "(Ljava/lang/String;I)I", (empty.clone(), -10)) + .await?, + 0 + ); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&string, "indexOf", "(Ljava/lang/String;I)I", (empty, 99)) + .await?, + 4 + ); + + Ok(()) +} + +#[tokio::test] +async fn test_unknown_string_charset_throws_unsupported_encoding() -> Result<()> { + let jvm = test_jvm().await?; + let string = JavaLangString::from_rust_string(&jvm, "value").await?; + let charset = JavaLangString::from_rust_string(&jvm, "not-a-charset").await?; + + let result: Result>> = jvm + .invoke_virtual(&string, "getBytes", "(Ljava/lang/String;)[B", (charset.clone(),)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("unknown charset must throw UnsupportedEncodingException"); + }; + assert!(jvm.is_instance(&*exception, "java/io/UnsupportedEncodingException")); + + let mut bytes = jvm.instantiate_array("B", 1).await?; + jvm.store_array(&mut bytes, 0, [b'a' as i8]).await?; + let result = jvm.new_class("java/lang/String", "([BLjava/lang/String;)V", (bytes, charset)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("unknown constructor charset must throw UnsupportedEncodingException"); + }; + assert!(jvm.is_instance(&*exception, "java/io/UnsupportedEncodingException")); + + Ok(()) +} + +#[tokio::test] +async fn test_trim_uses_java_control_character_boundary() -> Result<()> { + let jvm = test_jvm().await?; + let string = JavaLangString::from_rust_string(&jvm, " \t\u{a0}value\u{a0}\n ").await?; + let trimmed: ClassInstanceRef = jvm.invoke_virtual(&string, "trim", "()Ljava/lang/String;", ()).await?; + + assert_eq!(JavaLangString::to_rust_string(&jvm, &trimmed).await?, "\u{a0}value\u{a0}"); + + let unchanged = JavaLangString::from_rust_string(&jvm, "value").await?; + let same: ClassInstanceRef = jvm.invoke_virtual(&unchanged, "trim", "()Ljava/lang/String;", ()).await?; + assert_eq!(unchanged.identity(), same.identity()); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_system.rs b/java_runtime/tests/classes/java/lang/test_system.rs new file mode 100644 index 00000000..78309ac4 --- /dev/null +++ b/java_runtime/tests/classes/java/lang/test_system.rs @@ -0,0 +1,23 @@ +use alloc::collections::BTreeMap; + +use java_runtime::Runtime; +use jvm::Result; +use test_utils::{TestRuntime, create_test_jvm}; + +#[tokio::test] +async fn test_system_time_yield_and_exit_runtime_contract() -> Result<()> { + let runtime = TestRuntime::new(BTreeMap::new()); + let jvm = create_test_jvm(runtime.clone()).await?; + + let before = runtime.now(); + let now: i64 = jvm.invoke_static("java/lang/System", "currentTimeMillis", "()J", ()).await?; + assert!(now >= before as i64); + + let _: () = jvm.invoke_static("java/lang/Thread", "yield", "()V", ()).await?; + let _: () = jvm.invoke_static("java/lang/System", "exit", "(I)V", (17,)).await?; + assert_eq!(runtime.exit_status(), Some(17)); + let _: () = jvm.invoke_static("java/lang/System", "exit", "(I)V", (i32::MIN,)).await?; + assert_eq!(runtime.exit_status(), Some(i32::MIN)); + + Ok(()) +} diff --git a/jvm/src/array_class_instance.rs b/jvm/src/array_class_instance.rs index baf07ef6..80056f94 100644 --- a/jvm/src/array_class_instance.rs +++ b/jvm/src/array_class_instance.rs @@ -6,6 +6,8 @@ use crate::{Result, class_definition::ClassDefinition, class_instance::ClassInst pub trait ArrayClassInstance: ClassInstance { fn class_definition(&self) -> Box; fn destroy(self: Box); + fn identity(&self) -> usize; + fn shallow_clone(&self) -> Result>; fn equals(&self, other: &dyn ClassInstance) -> Result; fn store(&mut self, offset: usize, values: Box<[JavaValue]>) -> Result<()>; fn load(&self, offset: usize, count: usize) -> Result>; @@ -20,6 +22,14 @@ impl ClassInstance for T { ArrayClassInstance::destroy(self) } + fn identity(&self) -> usize { + ArrayClassInstance::identity(self) + } + + fn shallow_clone(&self) -> Result> { + ArrayClassInstance::shallow_clone(self) + } + fn class_definition(&self) -> Box { ArrayClassInstance::class_definition(self) } diff --git a/jvm/src/class_instance.rs b/jvm/src/class_instance.rs index f6248043..f1f0a91e 100644 --- a/jvm/src/class_instance.rs +++ b/jvm/src/class_instance.rs @@ -13,6 +13,8 @@ use crate::{ArrayClassInstance, ClassDefinition, Field, Result, as_any::AsAny, v #[async_trait::async_trait] pub trait ClassInstance: Sync + Send + AsAny + Debug + DynHash + DynClone + 'static { fn destroy(self: Box); + fn identity(&self) -> usize; + fn shallow_clone(&self) -> Result>; fn class_definition(&self) -> Box; fn equals(&self, other: &dyn ClassInstance) -> Result; fn get_field(&self, field: &dyn Field) -> Result; diff --git a/jvm/src/class_loader.rs b/jvm/src/class_loader.rs index 7e3c096e..aa739938 100644 --- a/jvm/src/class_loader.rs +++ b/jvm/src/class_loader.rs @@ -1,6 +1,7 @@ use alloc::{boxed::Box, sync::Arc}; -use parking_lot::RwLock; +use event_listener::{Event, EventListener}; +use parking_lot::{Mutex, RwLock}; use crate::{ ClassDefinition, ClassInstance, Jvm, Result, @@ -15,11 +16,29 @@ pub(crate) enum InitState { Erroneous, } +pub(crate) enum InitializationAction { + Initialize, + Recursive, + Wait(EventListener), + Initialized, + Erroneous, +} + +struct ClassInitializationState { + status: InitState, + owner: Option, +} + +struct ClassInitialization { + state: Mutex, + completed: Event, +} + #[derive(Clone)] pub struct Class { pub definition: Box, java_class: Arc>>>, - init_state: Arc>, + initialization: Arc, } impl Class { @@ -27,16 +46,40 @@ impl Class { Self { definition, java_class: Arc::new(RwLock::new(java_class)), - init_state: Arc::new(RwLock::new(InitState::NotInitialized)), + initialization: Arc::new(ClassInitialization { + state: Mutex::new(ClassInitializationState { + status: InitState::NotInitialized, + owner: None, + }), + completed: Event::new(), + }), } } - pub(crate) fn init_state(&self) -> InitState { - *self.init_state.read() + pub(crate) fn initialization_action(&self, thread_id: u64) -> InitializationAction { + let listener = self.initialization.completed.listen(); + let mut state = self.initialization.state.lock(); + + match state.status { + InitState::NotInitialized => { + state.status = InitState::InProgress; + state.owner = Some(thread_id); + InitializationAction::Initialize + } + InitState::InProgress if state.owner == Some(thread_id) => InitializationAction::Recursive, + InitState::InProgress => InitializationAction::Wait(listener), + InitState::Initialized => InitializationAction::Initialized, + InitState::Erroneous => InitializationAction::Erroneous, + } } - pub(crate) fn set_init_state(&self, state: InitState) { - *self.init_state.write() = state; + pub(crate) fn finish_initialization(&self, status: InitState) { + { + let mut state = self.initialization.state.lock(); + state.status = status; + state.owner = None; + } + self.initialization.completed.notify(usize::MAX); } pub fn set_java_class(&self, java_class: Box) { @@ -73,9 +116,11 @@ impl ClassLoaderWrapper for BootstrapClassLoaderWrapper<'_> { async fn load_class(&self, jvm: &Jvm, name: &str) -> Result> { let definition = self.bootstrap_class_loader.load_class(jvm, name).await?; if let Some(definition) = definition { - let java_class = jvm.register_class(definition.clone(), None).await?; + let java_class = JavaLangClass::from_rust_class(jvm, definition.clone(), None).await?; + let class = Class::new(definition, Some(java_class)); + jvm.register_class_internal(class.clone(), Some(self)).await?; - Ok(Some(Class::new(definition, java_class))) + Ok(Some(class)) } else { Ok(None) } diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index dba521b7..c6b88b4f 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -3,14 +3,12 @@ use alloc::{borrow::ToOwned, boxed::Box, collections::BTreeMap, format, string::String, sync::Arc, vec::Vec}; use core::{ fmt::Debug, - hash::BuildHasher, iter, sync::atomic::{AtomicBool, Ordering}, }; use dyn_clone::clone_box; -use event_listener::{Event, EventListener}; -use hashbrown::{DefaultHashBuilder, HashSet}; +use hashbrown::HashSet; use parking_lot::RwLock; use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; @@ -20,12 +18,15 @@ use crate::{ array_class_instance::{ArrayClassInstance, ArrayRawBuffer, ArrayRawBufferMut}, class_definition::ClassDefinition, class_instance::ClassInstance, - class_loader::{BootstrapClassLoader, BootstrapClassLoaderWrapper, Class, ClassLoaderWrapper, InitState, JavaClassLoaderWrapper}, + class_loader::{ + BootstrapClassLoader, BootstrapClassLoaderWrapper, Class, ClassLoaderWrapper, InitState, InitializationAction, JavaClassLoaderWrapper, + }, error::JavaError, field::Field, garbage_collector::determine_garbage, invoke_arg::InvokeArg, method::Method, + monitor::{Monitor, MonitorWait, MonitorWaitTimeout}, runtime::{JavaLangClass, JavaLangClassLoader, JavaLangString}, thread::JvmThread, r#type::JavaType, @@ -37,8 +38,7 @@ struct JvmInner { threads: RwLock>, all_objects: RwLock>>, string_pool: RwLock, Box>>, - monitors: RwLock>>, - monitor_hasher: DefaultHashBuilder, + monitors: RwLock>>, get_current_thread_id: Box u64 + Sync + Send>, bootstrap_class_loader: Box, bootstrapping: AtomicBool, @@ -62,7 +62,6 @@ impl Jvm { all_objects: RwLock::new(HashSet::new()), string_pool: RwLock::new(BTreeMap::new()), monitors: RwLock::new(BTreeMap::new()), - monitor_hasher: DefaultHashBuilder::default(), get_current_thread_id: Box::new(get_current_thread_id), bootstrap_class_loader: Box::new(bootstrap_class_loader), bootstrapping: AtomicBool::new(true), @@ -465,12 +464,28 @@ impl Jvm { pub fn destroy(&self, instance: Box) -> Result<()> { tracing::debug!("Destroy {}", instance.class_definition().name()); + self.inner.monitors.write().remove(&instance.identity()); self.inner.all_objects.write().remove(&instance); instance.destroy(); Ok(()) } + pub fn shallow_clone(&self, instance: &Box) -> Result> { + let cloned = instance.shallow_clone()?; + let thread_id = (self.inner.get_current_thread_id)(); + self.inner + .threads + .write() + .get_mut(&thread_id) + .unwrap() + .top_frame_mut() + .local_variables_mut() + .push(cloned.clone()); + self.inner.all_objects.write().insert(cloned.clone()); + Ok(cloned) + } + // JVMS 5.1 string interning: equal string literals (and String.intern results) share one instance pub async fn intern_string(&self, value: &str) -> Result> { let key = value.encode_utf16().collect::>(); @@ -512,19 +527,45 @@ impl Jvm { self.inner.classes.read().get(class_name).cloned() } - pub fn object_listen(&self, obj: &Box) -> EventListener { - self.get_or_create_monitor(obj).listen() + pub async fn monitor_enter(&self, obj: &Box) -> Result<()> { + let thread_id = (self.inner.get_current_thread_id)(); + self.get_or_create_monitor(obj).enter(thread_id).await; + Ok(()) + } + + pub async fn monitor_exit(&self, obj: &Box) -> Result<()> { + let thread_id = (self.inner.get_current_thread_id)(); + match self.get_or_create_monitor(obj).exit(thread_id) { + Ok(()) => Ok(()), + Err(_) => Err(self + .exception("java/lang/IllegalMonitorStateException", "current thread does not own the monitor") + .await), + } } - pub async fn object_wait(&self, obj: &Box) -> Result<()> { - self.object_listen(obj).await; + pub async fn object_wait_prepare(&self, obj: &Box) -> Result<(MonitorWait, MonitorWaitTimeout)> { + let thread_id = (self.inner.get_current_thread_id)(); + match self.get_or_create_monitor(obj).prepare_wait(thread_id) { + Ok(wait) => Ok(wait), + Err(_) => Err(self + .exception("java/lang/IllegalMonitorStateException", "current thread does not own the monitor") + .await), + } + } + pub async fn object_wait(&self, wait: MonitorWait) -> Result<()> { + wait.wait().await; Ok(()) } - pub fn object_notify(&self, obj: &Box, count: usize) { - let monitor = self.get_or_create_monitor(obj); - monitor.notify(count); + pub async fn object_notify(&self, obj: &Box, count: usize) -> Result<()> { + let thread_id = (self.inner.get_current_thread_id)(); + match self.get_or_create_monitor(obj).notify(thread_id, count) { + Ok(()) => Ok(()), + Err(_) => Err(self + .exception("java/lang/IllegalMonitorStateException", "current thread does not own the monitor") + .await), + } } #[async_recursion::async_recursion] @@ -681,6 +722,11 @@ impl Jvm { pub async fn exception(&self, r#type: &str, message: &str) -> JavaError { tracing::info!("throwing java exception: {} {message}", r#type); + if !self.has_class(r#type) { + self.resolve_class_internal(r#type, Some(&BootstrapClassLoaderWrapper::new(&*self.inner.bootstrap_class_loader))) + .await + .unwrap(); + } let message_str = JavaLangString::from_rust_string(self, message).await.unwrap(); let instance = self.new_class(r#type, "(Ljava/lang/String;)V", (message_str,)).await.unwrap(); @@ -734,7 +780,7 @@ impl Jvm { Ok(garbage_count) } - async fn register_class_internal(&self, class: Class, class_loader_wrapper: Option<&dyn ClassLoaderWrapper>) -> Result<()> { + pub(crate) async fn register_class_internal(&self, class: Class, class_loader_wrapper: Option<&dyn ClassLoaderWrapper>) -> Result<()> { if !class.definition.name().starts_with('[') { // ensure superclass and superinterfaces are loaded if let Some(super_class) = class.definition.super_class_name() @@ -750,7 +796,7 @@ impl Jvm { } } - self.inner.classes.write().insert(class.definition.name().to_owned(), class.clone()); + self.inner.classes.write().entry(class.definition.name().to_owned()).or_insert(class); Ok(()) } @@ -761,39 +807,41 @@ impl Jvm { return Ok(()); } - match class.init_state() { - InitState::Initialized | InitState::InProgress => return Ok(()), - InitState::Erroneous => { - return Err(self - .exception( - "java/lang/NoClassDefFoundError", - &format!("Could not initialize class {}", class.definition.name()), - ) - .await); + let thread_id = (self.inner.get_current_thread_id)(); + loop { + match class.initialization_action(thread_id) { + InitializationAction::Initialize => break, + InitializationAction::Recursive | InitializationAction::Initialized => return Ok(()), + InitializationAction::Wait(listener) => listener.await, + InitializationAction::Erroneous => { + return Err(self + .exception( + "java/lang/NoClassDefFoundError", + &format!("Could not initialize class {}", class.definition.name()), + ) + .await); + } } - InitState::NotInitialized => {} } - class.set_init_state(InitState::InProgress); - if let Some(super_name) = class.definition.super_class_name() { // resolution failure is not an initialization failure, so initialization may be retried let super_class = match self.resolve_class(&super_name).await { Ok(x) => x, Err(err) => { - class.set_init_state(InitState::NotInitialized); + class.finish_initialization(InitState::NotInitialized); return Err(err); } }; if let Err(err) = self.ensure_initialized(&super_class).await { - class.set_init_state(InitState::Erroneous); + class.finish_initialization(InitState::Erroneous); return Err(err); } } if let Err(err) = class.definition.prepare(self).await { - class.set_init_state(InitState::Erroneous); + class.finish_initialization(InitState::Erroneous); return Err(err); } @@ -801,7 +849,7 @@ impl Jvm { tracing::debug!("Calling for {}", class.definition.name()); if let Err(err) = self.execute_method(class, None, &clinit, Box::new([])).await { - class.set_init_state(InitState::Erroneous); + class.finish_initialization(InitState::Erroneous); let JavaError::JavaException(exception) = &err; if self.is_instance(&**exception, "java/lang/Error") { @@ -817,7 +865,7 @@ impl Jvm { } } - class.set_init_state(InitState::Initialized); + class.finish_initialization(InitState::Initialized); Ok(()) } @@ -890,8 +938,8 @@ impl Jvm { } } - fn get_or_create_monitor(&self, obj: &Box) -> Arc { - let key = self.inner.monitor_hasher.hash_one(obj); + fn get_or_create_monitor(&self, obj: &Box) -> Arc { + let key = obj.identity(); let monitors = self.inner.monitors.read(); if let Some(monitor) = monitors.get(&key) { @@ -900,7 +948,7 @@ impl Jvm { drop(monitors); let mut monitors = self.inner.monitors.write(); - monitors.entry(key).or_insert_with(|| Arc::new(Event::new())).clone() + monitors.entry(key).or_insert_with(|| Arc::new(Monitor::new())).clone() } // JVMS 5.4.3.2 field resolution: search the class, then its superinterfaces, then its superclass. @@ -979,6 +1027,15 @@ impl Jvm { let thread_id = (self.inner.get_current_thread_id)(); let method_str = format!("{}{}", method.name(), method.descriptor()); + let synchronized_object = if method.access_flags().contains(MethodAccessFlags::SYNCHRONIZED) { + Some(class_instance.clone().unwrap_or_else(|| class.java_class())) + } else { + None + }; + if let Some(object) = &synchronized_object { + self.monitor_enter(object).await?; + } + self.inner .threads .write() @@ -992,6 +1049,15 @@ impl Jvm { self.inner.threads.write().get_mut(&thread_id).unwrap().pop_frame(); + if let Some(object) = &synchronized_object + && let Err(error) = self.monitor_exit(object).await + { + if result.is_ok() { + return Err(error); + } + tracing::error!(?error, "failed to release synchronized method monitor"); + } + result } } diff --git a/jvm/src/lib.rs b/jvm/src/lib.rs index 80ec54d8..9706e426 100644 --- a/jvm/src/lib.rs +++ b/jvm/src/lib.rs @@ -13,6 +13,7 @@ mod garbage_collector; mod invoke_arg; mod jvm; mod method; +mod monitor; mod thread; mod r#type; mod value; @@ -39,6 +40,7 @@ pub use self::{ field::Field, jvm::Jvm, method::Method, + monitor::{MonitorWait, MonitorWaitTimeout}, r#type::JavaType, value::{JavaChar, JavaValue}, }; diff --git a/jvm/src/monitor.rs b/jvm/src/monitor.rs new file mode 100644 index 00000000..4d859c59 --- /dev/null +++ b/jvm/src/monitor.rs @@ -0,0 +1,299 @@ +use alloc::{collections::VecDeque, sync::Arc}; + +use event_listener::{Event, EventListener}; +use parking_lot::Mutex; + +pub(crate) struct Monitor { + state: Mutex, + entry_event: Event, +} + +struct MonitorState { + owner: Option, + depth: usize, + next_waiter_id: u64, + waiters: VecDeque, +} + +struct MonitorWaiter { + id: u64, + event: Arc, +} + +pub struct MonitorWait { + monitor: Arc, + listener: EventListener, + depth: usize, + thread_id: u64, +} + +#[derive(Clone)] +pub struct MonitorWaitTimeout { + monitor: Arc, + waiter_id: u64, + event: Arc, +} + +#[derive(Debug)] +pub(crate) enum MonitorError { + NotOwner, +} + +impl Monitor { + pub(crate) fn new() -> Self { + Self { + state: Mutex::new(MonitorState { + owner: None, + depth: 0, + next_waiter_id: 0, + waiters: VecDeque::new(), + }), + entry_event: Event::new(), + } + } + + pub(crate) async fn enter(self: &Arc, thread_id: u64) { + loop { + let listener = self.entry_event.listen(); + { + let mut state = self.state.lock(); + match state.owner { + None => { + state.owner = Some(thread_id); + state.depth = 1; + return; + } + Some(owner) if owner == thread_id => { + state.depth += 1; + return; + } + Some(_) => {} + } + } + listener.await; + } + } + + pub(crate) fn exit(&self, thread_id: u64) -> core::result::Result<(), MonitorError> { + let released = { + let mut state = self.state.lock(); + if state.owner != Some(thread_id) { + return Err(MonitorError::NotOwner); + } + + state.depth -= 1; + if state.depth == 0 { + state.owner = None; + true + } else { + false + } + }; + + if released { + self.entry_event.notify(1); + } + Ok(()) + } + + pub(crate) fn prepare_wait(self: &Arc, thread_id: u64) -> core::result::Result<(MonitorWait, MonitorWaitTimeout), MonitorError> { + let event = Arc::new(Event::new()); + let listener = event.listen(); + + let (waiter_id, depth) = { + let mut state = self.state.lock(); + if state.owner != Some(thread_id) { + return Err(MonitorError::NotOwner); + } + + let depth = state.depth; + let waiter_id = state.next_waiter_id; + state.next_waiter_id = state.next_waiter_id.wrapping_add(1); + state.waiters.push_back(MonitorWaiter { + id: waiter_id, + event: event.clone(), + }); + state.owner = None; + state.depth = 0; + + (waiter_id, depth) + }; + + self.entry_event.notify(1); + + Ok(( + MonitorWait { + monitor: self.clone(), + listener, + depth, + thread_id, + }, + MonitorWaitTimeout { + monitor: self.clone(), + waiter_id, + event, + }, + )) + } + + pub(crate) fn notify(&self, thread_id: u64, count: usize) -> core::result::Result<(), MonitorError> { + let events = { + let mut state = self.state.lock(); + if state.owner != Some(thread_id) { + return Err(MonitorError::NotOwner); + } + + let count = count.min(state.waiters.len()); + (0..count) + .filter_map(|_| state.waiters.pop_front()) + .map(|waiter| waiter.event) + .collect::>() + }; + + for event in events { + event.notify(1); + } + Ok(()) + } +} + +impl MonitorWait { + pub(crate) async fn wait(self) { + self.listener.await; + self.monitor.enter(self.thread_id).await; + self.monitor.state.lock().depth = self.depth; + } +} + +impl MonitorWaitTimeout { + pub fn notify(self) { + let event = { + let mut state = self.monitor.state.lock(); + state + .waiters + .iter() + .position(|waiter| waiter.id == self.waiter_id) + .and_then(|position| state.waiters.remove(position)) + .map(|waiter| waiter.event) + }; + + if let Some(event) = event { + debug_assert!(Arc::ptr_eq(&event, &self.event)); + event.notify(1); + } + } +} + +#[cfg(test)] +mod tests { + use alloc::sync::Arc; + use core::{ + sync::atomic::{AtomicBool, Ordering}, + time::Duration, + }; + + use super::Monitor; + + #[tokio::test] + async fn monitor_is_reentrant_and_excludes_other_threads() { + let monitor = Arc::new(Monitor::new()); + monitor.enter(1).await; + monitor.enter(1).await; + + let entered = Arc::new(AtomicBool::new(false)); + let contender = { + let monitor = monitor.clone(); + let entered = entered.clone(); + tokio::spawn(async move { + monitor.enter(2).await; + entered.store(true, Ordering::SeqCst); + monitor.exit(2).unwrap(); + }) + }; + + tokio::time::sleep(Duration::from_millis(10)).await; + assert!(!entered.load(Ordering::SeqCst)); + monitor.exit(1).unwrap(); + tokio::time::sleep(Duration::from_millis(10)).await; + assert!(!entered.load(Ordering::SeqCst)); + monitor.exit(1).unwrap(); + + tokio::time::timeout(Duration::from_secs(1), contender).await.unwrap().unwrap(); + assert!(entered.load(Ordering::SeqCst)); + } + + #[tokio::test] + async fn wait_releases_and_restores_the_full_reentrancy_depth() { + let monitor = Arc::new(Monitor::new()); + monitor.enter(1).await; + monitor.enter(1).await; + let (wait, _) = monitor.prepare_wait(1).unwrap(); + + monitor.enter(2).await; + monitor.notify(2, 1).unwrap(); + monitor.exit(2).unwrap(); + wait.wait().await; + + monitor.exit(1).unwrap(); + let entered = Arc::new(AtomicBool::new(false)); + let contender = { + let monitor = monitor.clone(); + let entered = entered.clone(); + tokio::spawn(async move { + monitor.enter(3).await; + entered.store(true, Ordering::SeqCst); + monitor.exit(3).unwrap(); + }) + }; + tokio::time::sleep(Duration::from_millis(10)).await; + assert!(!entered.load(Ordering::SeqCst)); + + monitor.exit(1).unwrap(); + tokio::time::timeout(Duration::from_secs(1), contender).await.unwrap().unwrap(); + assert!(entered.load(Ordering::SeqCst)); + } + + #[tokio::test] + async fn notify_one_and_notify_all_remove_the_expected_waiters() { + let monitor = Arc::new(Monitor::new()); + monitor.enter(1).await; + let (first_wait, _) = monitor.prepare_wait(1).unwrap(); + monitor.enter(2).await; + let (second_wait, _) = monitor.prepare_wait(2).unwrap(); + + monitor.enter(3).await; + monitor.notify(3, 1).unwrap(); + assert_eq!(monitor.state.lock().waiters.len(), 1); + monitor.exit(3).unwrap(); + first_wait.wait().await; + monitor.exit(1).unwrap(); + + monitor.enter(3).await; + monitor.notify(3, usize::MAX).unwrap(); + assert!(monitor.state.lock().waiters.is_empty()); + monitor.exit(3).unwrap(); + second_wait.wait().await; + monitor.exit(2).unwrap(); + } + + #[tokio::test] + async fn a_stale_timeout_cannot_consume_a_later_notification() { + let monitor = Arc::new(Monitor::new()); + monitor.enter(1).await; + let (first_wait, first_timeout) = monitor.prepare_wait(1).unwrap(); + first_timeout.clone().notify(); + first_wait.wait().await; + monitor.exit(1).unwrap(); + + monitor.enter(2).await; + let (second_wait, _) = monitor.prepare_wait(2).unwrap(); + first_timeout.notify(); + assert_eq!(monitor.state.lock().waiters.len(), 1); + + monitor.enter(3).await; + monitor.notify(3, 1).unwrap(); + monitor.exit(3).unwrap(); + second_wait.wait().await; + monitor.exit(2).unwrap(); + } +} diff --git a/jvm/src/type.rs b/jvm/src/type.rs index 5478529c..50c9fe10 100644 --- a/jvm/src/type.rs +++ b/jvm/src/type.rs @@ -8,7 +8,7 @@ use nom::{ IResult, Parser, bytes::complete::{take, take_until}, character::complete::anychar, - multi::many0, + error::{Error, ErrorKind}, sequence::terminated, }; @@ -49,7 +49,12 @@ impl JavaType { } pub fn parse(descriptor: &str) -> Self { - Self::parse_type(descriptor).unwrap().1 + Self::try_parse(descriptor).expect("invalid Java type descriptor") + } + + pub fn try_parse(descriptor: &str) -> Option { + let (remaining, r#type) = Self::parse_type(descriptor).ok()?; + if remaining.is_empty() { Some(r#type) } else { None } } // a CONSTANT_Class_info name (JVMS 4.4.1): a class binary name in internal form (java/lang/String) @@ -85,21 +90,39 @@ impl JavaType { 'D' => Ok((remaining, Self::Double)), 'L' => { let (remaining, class_name) = terminated(take_until(";"), take(1usize)).parse(remaining)?; + if class_name.is_empty() || class_name.contains(['.', '[', ';']) { + return Err(nom::Err::Error(Error::new(descriptor, ErrorKind::Verify))); + } Ok((remaining, Self::Class(class_name.to_string()))) } '[' => { let (remaining, element_type) = Self::parse_type(remaining)?; + if matches!(element_type, Self::Void | Self::Method(_, _)) { + return Err(nom::Err::Error(Error::new(descriptor, ErrorKind::Verify))); + } Ok((remaining, Self::Array(Box::new(element_type)))) } '(' => { let (remaining, params) = terminated(take_until(")"), take(1usize)).parse(remaining)?; - let param_types = many0(Self::parse_type).parse(params)?.1; + let mut param_types = Vec::new(); + let mut params = params; + while !params.is_empty() { + let (remaining_params, param_type) = Self::parse_type(params)?; + if remaining_params.len() >= params.len() || matches!(param_type, Self::Void | Self::Method(_, _)) { + return Err(nom::Err::Error(Error::new(descriptor, ErrorKind::Verify))); + } + param_types.push(param_type); + params = remaining_params; + } let (remaining, return_type) = Self::parse_type(remaining)?; + if matches!(return_type, Self::Method(_, _)) { + return Err(nom::Err::Error(Error::new(descriptor, ErrorKind::Verify))); + } Ok((remaining, Self::Method(param_types, Box::new(return_type)))) } - _ => panic!("Invalid type descriptor: {}", descriptor), + _ => Err(nom::Err::Error(Error::new(descriptor, ErrorKind::Verify))), } } } @@ -140,4 +163,14 @@ mod test { == JavaType::Array(Box::new(JavaType::Array(Box::new(JavaType::Class("java/lang/String".into()))))) ); } + + #[test] + fn test_try_parse_rejects_malformed_descriptors() { + assert!(JavaType::try_parse("").is_none()); + assert!(JavaType::try_parse("Igarbage").is_none()); + assert!(JavaType::try_parse("[V").is_none()); + assert!(JavaType::try_parse("(V)V").is_none()); + assert!(JavaType::try_parse("(I").is_none()); + assert!(JavaType::try_parse("L;").is_none()); + } } diff --git a/jvm_rust/src/array_class_instance.rs b/jvm_rust/src/array_class_instance.rs index eee85991..52bd8fd5 100644 --- a/jvm_rust/src/array_class_instance.rs +++ b/jvm_rust/src/array_class_instance.rs @@ -10,6 +10,7 @@ use jvm::{ArrayClassDefinition, ArrayClassInstance, ArrayRawBuffer, ArrayRawBuff use crate::array_class_definition::ArrayClassDefinitionImpl; +#[derive(Clone)] enum ArrayElements { Primitive(Vec), NonPrimitive(Vec), @@ -112,6 +113,21 @@ impl ArrayClassInstanceImpl { #[async_trait::async_trait] impl ArrayClassInstance for ArrayClassInstanceImpl { + fn identity(&self) -> usize { + Arc::as_ptr(&self.inner) as usize + } + + fn shallow_clone(&self) -> Result> { + Ok(Box::new(Self { + inner: Arc::new(ArrayClassInstanceInner { + class: self.inner.class.clone(), + length: self.inner.length, + element_type: self.inner.element_type.clone(), + elements: RwLock::new(self.inner.elements.read().clone()), + }), + })) + } + fn class_definition(&self) -> Box { self.inner.class.clone() } diff --git a/jvm_rust/src/class_definition.rs b/jvm_rust/src/class_definition.rs index 71cd3114..0162643e 100644 --- a/jvm_rust/src/class_definition.rs +++ b/jvm_rust/src/class_definition.rs @@ -12,7 +12,7 @@ use core::{ use parking_lot::RwLock; -use classfile::{AttributeInfo, ClassInfo, ConstantPoolReference}; +use classfile::{AttributeInfo, ClassFileError, ClassInfo, ConstantPoolReference, Opcode}; use java_class_proto::JavaClassProto; use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; use jvm::{ClassDefinition, ClassInstance, Field, JavaType, JavaValue, Jvm, Method, Result}; @@ -96,9 +96,128 @@ impl ClassDefinitionImpl { ) } - pub fn from_classfile(data: &[u8]) -> Result { - let class = ClassInfo::parse(data).unwrap(); // TODO ClassFormatError - assert_eq!(class.magic, 0xCAFEBABE); + pub fn from_classfile(data: &[u8]) -> core::result::Result { + let class = ClassInfo::parse(data)?; + + if class.this_class.is_empty() + || class.this_class.starts_with('[') + || class.super_class.as_ref().is_some_and(|name| name.is_empty() || name.starts_with('[')) + || class.interfaces.iter().any(|name| name.is_empty() || name.starts_with('[')) + { + return Err(ClassFileError::InvalidFormat); + } + for field in &class.fields { + let Some(r#type) = JavaType::try_parse(&field.descriptor) else { + return Err(ClassFileError::InvalidFormat); + }; + if matches!(r#type, JavaType::Void | JavaType::Method(_, _)) { + return Err(ClassFileError::InvalidFormat); + } + + let constant_values = field + .attributes + .iter() + .filter_map(|attribute| match attribute { + AttributeInfo::ConstantValue(value) => Some(value), + _ => None, + }) + .collect::>(); + if constant_values.len() > 1 + || constant_values.first().is_some_and(|value| { + !matches!( + (field.descriptor.as_str(), *value), + ("Z" | "B" | "C" | "S" | "I", ConstantPoolReference::Integer(_)) + | ("J", ConstantPoolReference::Long(_)) + | ("F", ConstantPoolReference::Float(_)) + | ("D", ConstantPoolReference::Double(_)) + | ("Ljava/lang/String;", ConstantPoolReference::String(_)) + ) + }) + { + return Err(ClassFileError::InvalidFormat); + } + } + for method in &class.methods { + if !matches!(JavaType::try_parse(&method.descriptor), Some(JavaType::Method(_, _))) { + return Err(ClassFileError::InvalidFormat); + } + + for attribute in &method.attributes { + let AttributeInfo::Code(code) = attribute else { + continue; + }; + for opcode in code.code.values() { + match opcode { + Opcode::Getfield(ConstantPoolReference::Field(reference)) + | Opcode::Getstatic(ConstantPoolReference::Field(reference)) + | Opcode::Putfield(ConstantPoolReference::Field(reference)) + | Opcode::Putstatic(ConstantPoolReference::Field(reference)) => { + let Some(r#type) = JavaType::try_parse(&reference.descriptor) else { + return Err(ClassFileError::InvalidFormat); + }; + if reference.class.is_empty() + || reference.class.starts_with('[') + || matches!(r#type, JavaType::Void | JavaType::Method(_, _)) + { + return Err(ClassFileError::InvalidFormat); + } + } + Opcode::Invokeinterface(ConstantPoolReference::InterfaceMethodref(reference), _, _) + | Opcode::Invokespecial(ConstantPoolReference::Method(reference)) + | Opcode::Invokestatic(ConstantPoolReference::Method(reference)) + | Opcode::Invokevirtual(ConstantPoolReference::Method(reference)) => { + if reference.class.is_empty() + || reference.class.starts_with('[') + || !matches!(JavaType::try_parse(&reference.descriptor), Some(JavaType::Method(_, _))) + { + return Err(ClassFileError::InvalidFormat); + } + } + Opcode::Anewarray(ConstantPoolReference::Class(name)) + | Opcode::Checkcast(ConstantPoolReference::Class(name)) + | Opcode::Instanceof(ConstantPoolReference::Class(name)) + | Opcode::Ldc(ConstantPoolReference::Class(name)) + | Opcode::LdcW(ConstantPoolReference::Class(name)) + | Opcode::New(ConstantPoolReference::Class(name)) => { + if name.is_empty() + || (name.starts_with('[') && !matches!(JavaType::try_parse(name), Some(JavaType::Array(_)))) + || (!name.starts_with('[') && name.contains(['.', ';', '['])) + { + return Err(ClassFileError::InvalidFormat); + } + } + Opcode::Multianewarray(ConstantPoolReference::Class(name), dimensions) => { + let Some(mut r#type) = JavaType::try_parse(name) else { + return Err(ClassFileError::InvalidFormat); + }; + let mut available_dimensions = 0; + while let JavaType::Array(element) = r#type { + available_dimensions += 1; + r#type = *element; + } + if available_dimensions < *dimensions as usize { + return Err(ClassFileError::InvalidFormat); + } + } + Opcode::Invokedynamic(_) => return Err(ClassFileError::InvalidFormat), + _ => {} + } + } + } + + let code_attributes = method + .attributes + .iter() + .filter(|attribute| matches!(attribute, AttributeInfo::Code(_))) + .count(); + if method.access_flags.intersects(MethodAccessFlags::ABSTRACT | MethodAccessFlags::NATIVE) { + if code_attributes != 0 { + return Err(ClassFileError::InvalidFormat); + } + } else if code_attributes != 1 { + return Err(ClassFileError::InvalidFormat); + } + } let mut constant_values = Vec::new(); let fields = class diff --git a/jvm_rust/src/class_instance.rs b/jvm_rust/src/class_instance.rs index 88e7eacd..d832142c 100644 --- a/jvm_rust/src/class_instance.rs +++ b/jvm_rust/src/class_instance.rs @@ -35,6 +35,19 @@ impl ClassInstanceImpl { impl ClassInstance for ClassInstanceImpl { fn destroy(self: Box) {} + fn identity(&self) -> usize { + Arc::as_ptr(&self.inner) as usize + } + + fn shallow_clone(&self) -> Result> { + Ok(Box::new(Self { + inner: Arc::new(ClassInstanceInner { + class: self.inner.class.clone(), + storage: RwLock::new(self.inner.storage.read().clone()), + }), + })) + } + fn class_definition(&self) -> Box { self.inner.class.clone() } diff --git a/jvm_rust/src/interpreter.rs b/jvm_rust/src/interpreter.rs index 0204b07a..43b822c6 100644 --- a/jvm_rust/src/interpreter.rs +++ b/jvm_rust/src/interpreter.rs @@ -858,12 +858,18 @@ impl Interpreter { return Ok(ExecuteNext::Jump((current_offset as i32 + *default) as u32)); } Opcode::Monitorenter => { - let stack_value = stack_frame.operand_stack.pop().unwrap(); - tracing::warn!("Unimplemented monitorenter{stack_value:?}"); + let object: Option> = stack_frame.operand_stack.pop().unwrap().into(); + let Some(object) = object else { + return Err(jvm.exception("java/lang/NullPointerException", "monitorenter on null").await); + }; + jvm.monitor_enter(&object).await?; } Opcode::Monitorexit => { - let stack_value = stack_frame.operand_stack.pop().unwrap(); - tracing::warn!("Unimplemented monitorexit{stack_value:?}"); + let object: Option> = stack_frame.operand_stack.pop().unwrap().into(); + let Some(object) = object else { + return Err(jvm.exception("java/lang/NullPointerException", "monitorexit on null").await); + }; + jvm.monitor_exit(&object).await?; } Opcode::Multianewarray(x, d) => { let mut dimensions: Vec = (0..*d).map(|_| stack_frame.operand_stack.pop().unwrap().into()).collect(); diff --git a/jvm_rust/src/lib.rs b/jvm_rust/src/lib.rs index 3c4c20f2..62fed997 100644 --- a/jvm_rust/src/lib.rs +++ b/jvm_rust/src/lib.rs @@ -16,3 +16,4 @@ pub use self::{ field::FieldImpl, method::{MethodBody, MethodImpl}, }; +pub use classfile::ClassFileError; diff --git a/jvm_rust/src/method.rs b/jvm_rust/src/method.rs index 7b2694a3..96a3ce98 100644 --- a/jvm_rust/src/method.rs +++ b/jvm_rust/src/method.rs @@ -133,7 +133,14 @@ impl Method for MethodImpl { } async fn run(&self, jvm: &Jvm, args: Box<[JavaValue]>) -> Result { - Ok(match &self.inner.body.as_ref().unwrap() { + let Some(body) = self.inner.body.as_ref() else { + if self.inner.access_flags.contains(MethodAccessFlags::NATIVE) { + return Err(jvm.exception("java/lang/UnsatisfiedLinkError", &self.inner.name).await); + } + return Err(jvm.exception("java/lang/AbstractMethodError", &self.inner.name).await); + }; + + Ok(match body { MethodBody::ByteCode(x) => { let r#type = JavaType::parse(&self.inner.descriptor); Interpreter::run(jvm, x, args, r#type.as_method().1).await? diff --git a/src/runtime.rs b/src/runtime.rs index 921baa4c..95bf6893 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1,6 +1,6 @@ mod io; -use alloc::{collections::BTreeMap, sync::Arc}; +use alloc::{collections::BTreeMap, format, sync::Arc}; use core::{ sync::atomic::{AtomicU32, AtomicU64, Ordering}, time::Duration, @@ -9,11 +9,12 @@ use std::{ fs, io::{Write, stderr, stdin}, sync::Mutex, + time::{SystemTime, UNIX_EPOCH}, }; use java_runtime::{File, FileDescriptorId, FileStat, FileType, IOError, IOResult, RT_RUSTJAR, Runtime, SpawnCallback, get_runtime_class_proto}; use jvm::{ClassDefinition, Jvm}; -use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionImpl}; +use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionImpl, ClassFileError}; use self::io::{FileImpl, InputStreamFile, WriteStreamFile}; @@ -87,12 +88,12 @@ impl Runtime for RuntimeImpl where T: Sync + Send + Write + 'static, { - async fn sleep(&self, _duration: Duration) { - todo!() + async fn sleep(&self, duration: Duration) { + tokio::time::sleep(duration).await; } async fn r#yield(&self) { - todo!() + tokio::task::yield_now().await; } fn spawn(&self, _jvm: &Jvm, callback: Box) { @@ -100,14 +101,20 @@ where tokio::spawn(async move { TASK_ID .scope(task_id, async move { - callback.call().await.unwrap(); + if let Err(error) = callback.call().await { + tracing::error!(?error, "spawned Java task failed"); + } }) .await; }); } + fn exit(&self, status: i32) { + std::process::exit(status); + } + fn now(&self) -> u64 { - todo!() + SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(Duration::ZERO).as_millis() as u64 } fn current_task_id(&self) -> u64 { @@ -174,8 +181,17 @@ where Ok(None) } - async fn define_class(&self, _jvm: &Jvm, data: &[u8]) -> jvm::Result> { - ClassDefinitionImpl::from_classfile(data).map(|x| Box::new(x) as Box<_>) + async fn define_class(&self, jvm: &Jvm, data: &[u8]) -> jvm::Result> { + match ClassDefinitionImpl::from_classfile(data) { + Ok(class) => Ok(Box::new(class)), + Err(ClassFileError::InvalidFormat) => Err(jvm.exception("java/lang/ClassFormatError", "Invalid class file").await), + Err(ClassFileError::UnsupportedVersion(version)) => Err(jvm + .exception( + "java/lang/UnsupportedClassVersionError", + &format!("Unsupported class file version {version}"), + ) + .await), + } } async fn define_array_class(&self, _jvm: &Jvm, element_type_name: &str) -> jvm::Result> { diff --git a/test_data/MonitorSemantics$1.class b/test_data/MonitorSemantics$1.class new file mode 100644 index 0000000000000000000000000000000000000000..f3746ff4d3a8687140eb508f688205897f06c481 GIT binary patch literal 196 zcmZ8b!3x4K6wBMDol|fK-aUAB2>yX6co-h`1+|vhQCn!MALqdj@T0`e;{r)8Nyz8> zdIQ*D%8(EiwKXbO*T_3JK?%R!GGv6Z<4;~`ZrbX6>7)pRqOlJr$A)3T>|~5|d(FL(^b literal 0 HcmV?d00001 diff --git a/test_data/MonitorSemantics$CounterWorker.class b/test_data/MonitorSemantics$CounterWorker.class new file mode 100644 index 0000000000000000000000000000000000000000..e9702cd0845a90891006a431aacb8269e6095e61 GIT binary patch literal 970 zcmZuwOHUJF6g_u3Wy&x}EiaLWfJIxZv_&=wVMAilq-Y`p;?80CA_HZ{Os78f{t`E= zS=kt)iR}CX{ug4z^G%6BWifN#=bn4#&d)zzz5!Umk_`hX3#NlKdIVGJVc^GMw4n~& zAojgxel2VTv5L0C=s-n+o)w=7tAbRaxMd@wi+csxefOX4@Velfms*TP+jfw~{4XRX5>6>W9` zxq9fijV(9wwSDe1<2}DAn67uvd3ArwV32kY*e1ku90 zVDN$$XA0kS`i~a=Z&F<@ZV8O{dXv{hIr>J}iahny*Ru_Gvs}=f1WqjoRJ7J`n@z=O z`mcO@3y-*}?#g@>(>$RJ1e3g|6m=dFtvj^JltP)F{|xJxios+gVLCBG-RUG?7I$e0 z^8bpzCQt-Pt&C?EKcU!${c`MJ_<3LT2u+W?Av+o&AfSD|Nisy7l7B;3E-gS!3|Nzion)UW^|e5BQ=$#)Jf4D zNS$hP{58u@RW7jdR6YJhAv5= zNc4cH$0!)W4IMiW7=3Oz^^I+(E=w<6d)p`Am8=Q$nd!;^wc`c`pbKcWx95h^7Z3$?Zy30M zj|8H{_56y!P;tk4VP)%v&;s3U=oH4GUTZu;&82*j2ghI3!Ts6etKenLl1 zLq6QMwPPwP6yFr_f5CZ^+tVY}95UA8gP20m4J_XnZn zhe|7LU`E|UcRa_{FneA&%j^cTI1VR+N!7xvft=EeS+!bOx&v6iXBuuBxP#A6rCNF! zgtEczd3zzt)2fc+WgoLNAqym}2K#as_cVNIU=d3K!_5p%W7zIprBQbotyxG{Nb+7y z3S2BYu3Xz|Y)ZeZl2x7eY^z?ed`I!^%V@amaJYuflyH%j8kR%5m&|A9HIH)3-=gmD zIbv4xx#I^+t$W^D{iWqtAZ^d(@M>zPSw;c$ua|bi8Fnk?_?yH4Xop=}7r})_3g2Kj^Y{tSl=Y7fB^KH4z*T(Mjj@K>K2?3+v zy3)_rtss!B!-Z25s^J-v@6}Ar6L)HKaGhh5;7=feqFObq4uEAqjQdyQvZR8v zf94Pu-|!H@eeOyOuCKWpEdUSBo%wdI9EAv(rXQmY#We>?ExG&>-rZma7`G}_oHAhk4sSiI85v6P;5yvXl+C>>B zRbTV~gRe=u%O{9j$K$g|?T>SiKjKLnnWJB2HI}Th$O`*^ZH=L%rhY_B&r;sw;1eIh!9dCB!Uh6M4DtFCdgIk_GhI_*BSD^K>rVA CsZ_-P literal 0 HcmV?d00001 diff --git a/test_data/MonitorSemantics.txt b/test_data/MonitorSemantics.txt new file mode 100644 index 00000000..bc6298e8 --- /dev/null +++ b/test_data/MonitorSemantics.txt @@ -0,0 +1 @@ +110 diff --git a/test_data/NativeMethod.class b/test_data/NativeMethod.class new file mode 100644 index 0000000000000000000000000000000000000000..2798b674ba3876136a9610a7b19d4f0f35d006d5 GIT binary patch literal 727 zcmZvZO>fgc5Qg8i(>igTv`uKjXDQ!Fk;sKx5Eld|ryvC)p`JF$l5QJ+Xl+;Ui#V49 zkwD@H@S_m#x|Il0WbLeHcITOQXMX?r@e9CnJoVwg^-u}0flYy(Q+Y1?3mMP*Zzrc} znhR{c(6P>63b@_g*h5tyIFh+OS3{MbBr_i#Tx+%h1Qy{%I?HrC_s|g7|Hu0fXH3gJ zX*COVeD>Oy#MH2ZT@TFwEnNGr`shoRtB6IDf*SV1lB1LUJEP-#lp7_ZK^1L*ts>5f zG)+vdsO36tc(@th7H$i))+AA1Y7vW=)b1s=d@h+3J?M=EDzB2664<6pbyP$XW!}rl zf{|vJOyy!Mjkf+(u#%tXOrR02c4a_HBsGh)yT|LRRMqE{4OTOOgOwZW+GJ_c%vq(i z1}0D+<#Kv9lvI>}Hx8E&7yNOs zPiv3Cb(c1{ynq8jARIe#Mr*i_2RyNmlvx(u`3dji6164#uL#0#*lzB9N9Uph_^9)9 vOMIIOY!RQBI+ZoR!P2wk+C*9ZAs&@^9X!Sp#=Bg6!_Y&$%7Y$V2Zw(FlJ1+$ literal 0 HcmV?d00001 diff --git a/test_data/NativeMethod.txt b/test_data/NativeMethod.txt new file mode 100644 index 00000000..ad7ccf7a --- /dev/null +++ b/test_data/NativeMethod.txt @@ -0,0 +1 @@ +unsupported diff --git a/test_data/src/MonitorSemantics.java b/test_data/src/MonitorSemantics.java new file mode 100644 index 00000000..9dc5451d --- /dev/null +++ b/test_data/src/MonitorSemantics.java @@ -0,0 +1,49 @@ +public class MonitorSemantics { + private static final Object LOCK = new Object(); + private static int counter; + + private static class CounterWorker implements Runnable { + public void run() { + for (int i = 0; i < 50; i++) { + synchronized (LOCK) { + int current = counter; + Thread.yield(); + counter = current + 1; + } + } + } + } + + private static class FailingWorker implements Runnable { + public void run() { + try { + failWhileSynchronized(); + } catch (RuntimeException expected) { + } + } + } + + private static synchronized void failWhileSynchronized() { + throw new RuntimeException("expected"); + } + + private static synchronized void addTen() { + counter += 10; + } + + public static void main(String[] args) throws Exception { + Thread first = new Thread(new CounterWorker()); + Thread second = new Thread(new CounterWorker()); + first.start(); + second.start(); + first.join(); + second.join(); + + Thread failing = new Thread(new FailingWorker()); + failing.start(); + failing.join(); + addTen(); + + System.out.println(counter); + } +} diff --git a/test_data/src/NativeMethod.java b/test_data/src/NativeMethod.java new file mode 100644 index 00000000..335a8d1d --- /dev/null +++ b/test_data/src/NativeMethod.java @@ -0,0 +1,11 @@ +public class NativeMethod { + private native void missing(); + + public static void main(String[] args) { + try { + new NativeMethod().missing(); + } catch (UnsatisfiedLinkError expected) { + System.out.println("unsupported"); + } + } +} diff --git a/test_utils/Cargo.toml b/test_utils/Cargo.toml index 7e798257..11a2d435 100644 --- a/test_utils/Cargo.toml +++ b/test_utils/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true [dependencies] async-trait = { workspace = true } +tracing = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "time"] } jvm = { workspace = true } diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs index 6b0ca6bc..f37b8a5f 100644 --- a/test_utils/src/lib.rs +++ b/test_utils/src/lib.rs @@ -1,9 +1,9 @@ extern crate alloc; -use alloc::{boxed::Box, collections::BTreeMap, string::String, sync::Arc, vec::Vec}; +use alloc::{boxed::Box, collections::BTreeMap, format, string::String, sync::Arc, vec::Vec}; use core::{ cmp::min, - sync::atomic::{AtomicU32, AtomicU64, Ordering}, + sync::atomic::{AtomicI64, AtomicU32, AtomicU64, Ordering}, time::Duration, }; use std::{ @@ -12,7 +12,7 @@ use std::{ }; use jvm::{ClassDefinition, Jvm, Result}; -use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionImpl}; +use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionImpl, ClassFileError}; use java_runtime::{ File, FileDescriptorId, FileSize, FileStat, FileType, IOError, IOResult, RT_RUSTJAR, Runtime, SpawnCallback, get_bootstrap_class_loader, @@ -23,6 +23,7 @@ pub struct TestRuntime { filesystem: BTreeMap>, file_table: Arc>>>, next_fd: Arc, + exit_status: Arc, } impl Clone for TestRuntime { @@ -31,6 +32,7 @@ impl Clone for TestRuntime { filesystem: self.filesystem.clone(), file_table: self.file_table.clone(), next_fd: self.next_fd.clone(), + exit_status: self.exit_status.clone(), } } } @@ -41,9 +43,15 @@ impl TestRuntime { filesystem, file_table: Arc::new(Mutex::new(BTreeMap::new())), next_fd: Arc::new(AtomicU32::new(1)), + exit_status: Arc::new(AtomicI64::new(i64::MIN)), } } + pub fn exit_status(&self) -> Option { + let status = self.exit_status.load(Ordering::SeqCst); + (status != i64::MIN).then_some(status as i32) + } + fn register_file(&self, file: Box) -> FileDescriptorId { let fd = self.next_fd.fetch_add(1, Ordering::SeqCst); self.file_table.lock().unwrap().insert(fd, file); @@ -64,7 +72,7 @@ impl Runtime for TestRuntime { } async fn r#yield(&self) { - todo!() + tokio::task::yield_now().await; } fn spawn(&self, _jvm: &Jvm, callback: Box) { @@ -72,12 +80,18 @@ impl Runtime for TestRuntime { tokio::spawn(async move { TASK_ID .scope(task_id, async move { - callback.call().await.unwrap(); + if let Err(error) = callback.call().await { + tracing::error!(?error, "spawned Java test task failed"); + } }) .await; }); } + fn exit(&self, status: i32) { + self.exit_status.store(status as i64, Ordering::SeqCst); + } + fn now(&self) -> u64 { SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(Duration::from_secs(0)).as_millis() as u64 } @@ -146,8 +160,17 @@ impl Runtime for TestRuntime { Ok(None) } - async fn define_class(&self, _jvm: &Jvm, data: &[u8]) -> jvm::Result> { - ClassDefinitionImpl::from_classfile(data).map(|x| Box::new(x) as Box<_>) + async fn define_class(&self, jvm: &Jvm, data: &[u8]) -> jvm::Result> { + match ClassDefinitionImpl::from_classfile(data) { + Ok(class) => Ok(Box::new(class)), + Err(ClassFileError::InvalidFormat) => Err(jvm.exception("java/lang/ClassFormatError", "Invalid class file").await), + Err(ClassFileError::UnsupportedVersion(version)) => Err(jvm + .exception( + "java/lang/UnsupportedClassVersionError", + &format!("Unsupported class file version {version}"), + ) + .await), + } } async fn define_array_class(&self, _jvm: &Jvm, element_type_name: &str) -> jvm::Result> { From d361c9eac53a3b05fa7b3c76f2376f32f702a6ef Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Fri, 17 Jul 2026 16:09:50 +0900 Subject: [PATCH 2/5] Address classfile review findings --- classfile/src/opcode.rs | 8 ++++---- classfile/tests/test.rs | 12 ++++++++++++ jvm_rust/src/class_definition.rs | 11 +++++++++-- test_data/Array.class | Bin 830 -> 934 bytes test_data/Array.txt | 2 ++ test_data/src/Array.java | 5 +++++ 6 files changed, 32 insertions(+), 6 deletions(-) diff --git a/classfile/src/opcode.rs b/classfile/src/opcode.rs index f20b8d41..cdc6733a 100644 --- a/classfile/src/opcode.rs +++ b/classfile/src/opcode.rs @@ -406,8 +406,8 @@ impl Opcode { 0x69 => success(Opcode::Lmul).parse(data), 0x75 => success(Opcode::Lneg).parse(data), 0xab => flat_map((take((4 - (offset + 1) % 4) % 4), be_i32, be_i32), |(_, default, npairs)| { - move |x| { - if npairs < 0 { + move |x: &'a [u8]| { + if npairs < 0 || npairs as usize > x.len() / 8 { return Err(nom::Err::Error(Error::new(x, ErrorKind::Verify))); } map(count((be_i32, be_i32), npairs as usize), |offsets| Opcode::Lookupswitch(default, offsets)).parse(x) @@ -469,11 +469,11 @@ impl Opcode { 0x11 => map(be_i16, Opcode::Sipush).parse(data), 0x5f => success(Opcode::Swap).parse(data), 0xaa => flat_map((take((4 - (offset + 1) % 4) % 4), be_i32, be_i32, be_i32), |(_, default, low, high)| { - move |x| { + move |x: &'a [u8]| { let Some(entry_count) = high.checked_sub(low).and_then(|range| range.checked_add(1)) else { return Err(nom::Err::Error(Error::new(x, ErrorKind::Verify))); }; - if entry_count < 0 { + if entry_count <= 0 || entry_count as usize > x.len() / 4 { return Err(nom::Err::Error(Error::new(x, ErrorKind::Verify))); } map(count(be_i32, entry_count as usize), |offsets| { diff --git a/classfile/tests/test.rs b/classfile/tests/test.rs index 0caba744..bd78f10d 100644 --- a/classfile/tests/test.rs +++ b/classfile/tests/test.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use java_constants::ClassAccessFlags; use classfile::{AttributeInfo, ClassFileError, ClassInfo, ConstantPoolReference, Opcode}; @@ -114,6 +116,16 @@ fn test_switch() { } } +#[test] +fn test_switch_rejects_entry_counts_larger_than_remaining_input() { + let constant_pool = BTreeMap::new(); + let lookup_switch = [0xab, 0, 0, 0, 0, 0, 0, 0, 0x7f, 0xff, 0xff, 0xff]; + assert!(Opcode::parse(&lookup_switch, 0, &constant_pool).is_err()); + + let table_switch = [0xaa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x7f, 0xff, 0xff, 0xff]; + assert!(Opcode::parse(&table_switch, 0, &constant_pool).is_err()); +} + #[test] fn test_invokeinterface() { let interface = include_bytes!("../../test_data/Interface.class"); diff --git a/jvm_rust/src/class_definition.rs b/jvm_rust/src/class_definition.rs index 0162643e..d248505c 100644 --- a/jvm_rust/src/class_definition.rs +++ b/jvm_rust/src/class_definition.rs @@ -164,8 +164,7 @@ impl ClassDefinitionImpl { } Opcode::Invokeinterface(ConstantPoolReference::InterfaceMethodref(reference), _, _) | Opcode::Invokespecial(ConstantPoolReference::Method(reference)) - | Opcode::Invokestatic(ConstantPoolReference::Method(reference)) - | Opcode::Invokevirtual(ConstantPoolReference::Method(reference)) => { + | Opcode::Invokestatic(ConstantPoolReference::Method(reference)) => { if reference.class.is_empty() || reference.class.starts_with('[') || !matches!(JavaType::try_parse(&reference.descriptor), Some(JavaType::Method(_, _))) @@ -173,6 +172,14 @@ impl ClassDefinitionImpl { return Err(ClassFileError::InvalidFormat); } } + Opcode::Invokevirtual(ConstantPoolReference::Method(reference)) => { + if reference.class.is_empty() + || (reference.class.starts_with('[') && !matches!(JavaType::try_parse(&reference.class), Some(JavaType::Array(_)))) + || !matches!(JavaType::try_parse(&reference.descriptor), Some(JavaType::Method(_, _))) + { + return Err(ClassFileError::InvalidFormat); + } + } Opcode::Anewarray(ConstantPoolReference::Class(name)) | Opcode::Checkcast(ConstantPoolReference::Class(name)) | Opcode::Instanceof(ConstantPoolReference::Class(name)) diff --git a/test_data/Array.class b/test_data/Array.class index 288ed32a68064375ec9ea935f43019f1db248a99..6eb383b79c03d42f9efd2f107193f5f6ec99a081 100644 GIT binary patch delta 240 zcmdnTwv3(Y)W2Q(7#J8F8O%0v=`&h#F=#MovNLG$FlaOAFfuSjdonVxCg-#5Vr6!kHGc$1h{>aXtKiP&c&P$Jh31}7rm}F&OV_;-30Ps0}BHOQ1rrNDW>zh2(@JlvXdp4i@BA6 L>U4qfObmJeNG=ej diff --git a/test_data/Array.txt b/test_data/Array.txt index b3e7d36f..b7a9bf71 100644 --- a/test_data/Array.txt +++ b/test_data/Array.txt @@ -12,3 +12,5 @@ test한글 10 10 10 +112344 +654321 diff --git a/test_data/src/Array.java b/test_data/src/Array.java index 1c26b090..8038ff84 100644 --- a/test_data/src/Array.java +++ b/test_data/src/Array.java @@ -31,5 +31,10 @@ public static void main(String[] args) { System.out.println(char_array.length); System.out.println(boolean_array.length); System.out.println(string_array.length); + + int[] cloned_int_array = (int[]) int_array.clone(); + cloned_int_array[0] = 654321; + System.out.println(int_array[0]); + System.out.println(cloned_int_array[0]); } } From 8836e4159f9eae257383db6cc66bbb7e71644cac Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Fri, 17 Jul 2026 17:30:58 +0900 Subject: [PATCH 3/5] Move class initialization tests to Java fixture --- AGENTS.md | 5 + java_runtime/tests/classes/java/lang/mod.rs | 1 - .../java/lang/test_class_initialization.rs | 238 ------------------ test_data/ConcurrentClinit$Failing.class | Bin 0 -> 783 bytes .../ConcurrentClinit$FailingReader.class | Bin 0 -> 809 bytes test_data/ConcurrentClinit$Successful.class | Bin 0 -> 814 bytes .../ConcurrentClinit$SuccessfulReader.class | Bin 0 -> 616 bytes test_data/ConcurrentClinit.class | Bin 0 -> 1740 bytes test_data/ConcurrentClinit.txt | 6 + test_data/src/ConcurrentClinit.java | 120 +++++++++ 10 files changed, 131 insertions(+), 239 deletions(-) delete mode 100644 java_runtime/tests/classes/java/lang/test_class_initialization.rs create mode 100644 test_data/ConcurrentClinit$Failing.class create mode 100644 test_data/ConcurrentClinit$FailingReader.class create mode 100644 test_data/ConcurrentClinit$Successful.class create mode 100644 test_data/ConcurrentClinit$SuccessfulReader.class create mode 100644 test_data/ConcurrentClinit.class create mode 100644 test_data/ConcurrentClinit.txt create mode 100644 test_data/src/ConcurrentClinit.java diff --git a/AGENTS.md b/AGENTS.md index a112a0ce..409e9c0b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,3 +22,8 @@ - `classfile/` - Class file parser - `java_class_proto/` - Java class prototypes - `test_utils/` - Shared test utilities + +## Testing Boundaries +- Keep `java_runtime/tests/classes` limited to Java standard library class and API behavior. +- Test JVM and interpreter semantics, including class initialization, bytecode execution, and monitor behavior, with compiled Java fixtures under `test_data/src` and expected output under `test_data`, executed by `tests/test_class.rs`. +- Do not place JVM core behavior tests in the `java_runtime` standard library test tree. diff --git a/java_runtime/tests/classes/java/lang/mod.rs b/java_runtime/tests/classes/java/lang/mod.rs index ced07ebc..5de9e808 100644 --- a/java_runtime/tests/classes/java/lang/mod.rs +++ b/java_runtime/tests/classes/java/lang/mod.rs @@ -2,7 +2,6 @@ mod test_boolean; mod test_byte; mod test_character; mod test_class; -mod test_class_initialization; mod test_cldc11_exceptions; mod test_double; mod test_float; diff --git a/java_runtime/tests/classes/java/lang/test_class_initialization.rs b/java_runtime/tests/classes/java/lang/test_class_initialization.rs deleted file mode 100644 index 206cbe2a..00000000 --- a/java_runtime/tests/classes/java/lang/test_class_initialization.rs +++ /dev/null @@ -1,238 +0,0 @@ -use alloc::{boxed::Box, collections::BTreeMap, sync::Arc, vec}; -use core::{ - sync::atomic::{AtomicUsize, Ordering}, - time::Duration, -}; -use std::sync::Mutex; - -use java_class_proto::{JavaFieldProto, JavaMethodProto}; -use java_constants::{FieldAccessFlags, MethodAccessFlags}; -use java_runtime::{Runtime, RuntimeClassProto, RuntimeContext, SpawnCallback}; -use jvm::{JavaError, Jvm, Result}; -use jvm_rust::ClassDefinitionImpl; -use test_utils::{TestRuntime, create_test_jvm}; - -struct ConcurrentInitialization; - -static FAILING_INITIALIZATION_CALLS: AtomicUsize = AtomicUsize::new(0); - -impl ConcurrentInitialization { - fn as_proto() -> RuntimeClassProto { - RuntimeClassProto { - name: "ConcurrentInitialization", - parent_class: Some("java/lang/Object"), - interfaces: vec![], - methods: vec![JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC)], - fields: vec![ - JavaFieldProto::new("count", "I", FieldAccessFlags::STATIC), - JavaFieldProto::new("value", "I", FieldAccessFlags::STATIC), - ], - access_flags: Default::default(), - } - } - - async fn clinit(jvm: &Jvm, context: &mut RuntimeContext) -> Result<()> { - let count: i32 = jvm.get_static_field("ConcurrentInitialization", "count", "I").await?; - jvm.put_static_field("ConcurrentInitialization", "count", "I", count + 1).await?; - context.sleep(Duration::from_millis(50)).await; - jvm.put_static_field("ConcurrentInitialization", "value", "I", 42i32).await - } -} - -struct RecursiveInitialization; - -impl RecursiveInitialization { - fn as_proto() -> RuntimeClassProto { - RuntimeClassProto { - name: "RecursiveInitialization", - parent_class: Some("java/lang/Object"), - interfaces: vec![], - methods: vec![JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC)], - fields: vec![ - JavaFieldProto::new("count", "I", FieldAccessFlags::STATIC), - JavaFieldProto::new("value", "I", FieldAccessFlags::STATIC), - ], - access_flags: Default::default(), - } - } - - async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { - let count: i32 = jvm.get_static_field("RecursiveInitialization", "count", "I").await?; - jvm.put_static_field("RecursiveInitialization", "count", "I", count + 1).await?; - jvm.put_static_field("RecursiveInitialization", "value", "I", 7i32).await - } -} - -struct FailingInitialization; - -impl FailingInitialization { - fn as_proto() -> RuntimeClassProto { - RuntimeClassProto { - name: "FailingInitialization", - parent_class: Some("java/lang/Object"), - interfaces: vec![], - methods: vec![JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC)], - fields: vec![JavaFieldProto::new("value", "I", FieldAccessFlags::STATIC)], - access_flags: Default::default(), - } - } - - async fn clinit(jvm: &Jvm, context: &mut RuntimeContext) -> Result<()> { - FAILING_INITIALIZATION_CALLS.fetch_add(1, Ordering::SeqCst); - context.sleep(Duration::from_millis(50)).await; - Err(jvm.exception("java/lang/IllegalArgumentException", "initialization failed").await) - } -} - -#[tokio::test] -async fn class_initialization_waits_for_the_owner_thread() -> Result<()> { - let runtime = TestRuntime::new(BTreeMap::new()); - let jvm = create_test_jvm(runtime.clone()).await?; - jvm.register_class( - Box::new(ClassDefinitionImpl::from_class_proto( - ConcurrentInitialization::as_proto(), - Box::new(runtime.clone()) as Box<_>, - )), - None, - ) - .await?; - - struct ReadValue { - jvm: Jvm, - completed: Arc, - failures: Arc, - values: Arc>>, - } - - #[async_trait::async_trait] - impl SpawnCallback for ReadValue { - async fn call(&self) -> Result<()> { - self.jvm.attach_thread(None).await?; - match self.jvm.get_static_field("ConcurrentInitialization", "value", "I").await { - Ok(value) => self.values.lock().unwrap().push(value), - Err(_) => { - self.failures.fetch_add(1, Ordering::SeqCst); - } - } - self.jvm.detach_thread()?; - self.completed.fetch_add(1, Ordering::SeqCst); - Ok(()) - } - } - - let completed = Arc::new(AtomicUsize::new(0)); - let failures = Arc::new(AtomicUsize::new(0)); - let values = Arc::new(Mutex::new(alloc::vec::Vec::new())); - for _ in 0..2 { - runtime.spawn( - &jvm, - Box::new(ReadValue { - jvm: jvm.clone(), - completed: completed.clone(), - failures: failures.clone(), - values: values.clone(), - }), - ); - } - - for _ in 0..100 { - if completed.load(Ordering::SeqCst) == 2 { - break; - } - runtime.sleep(Duration::from_millis(5)).await; - } - - assert_eq!(completed.load(Ordering::SeqCst), 2); - assert_eq!(failures.load(Ordering::SeqCst), 0); - assert_eq!(*values.lock().unwrap(), [42, 42]); - assert_eq!(jvm.get_static_field::("ConcurrentInitialization", "count", "I").await?, 1); - - Ok(()) -} - -#[tokio::test] -async fn class_initialization_allows_same_thread_recursion() -> Result<()> { - let runtime = TestRuntime::new(BTreeMap::new()); - let jvm = create_test_jvm(runtime.clone()).await?; - jvm.register_class( - Box::new(ClassDefinitionImpl::from_class_proto( - RecursiveInitialization::as_proto(), - Box::new(runtime) as Box<_>, - )), - None, - ) - .await?; - - assert_eq!(jvm.get_static_field::("RecursiveInitialization", "value", "I").await?, 7); - assert_eq!(jvm.get_static_field::("RecursiveInitialization", "count", "I").await?, 1); - - Ok(()) -} - -#[tokio::test] -async fn failed_class_initialization_wakes_waiters_and_becomes_erroneous() -> Result<()> { - FAILING_INITIALIZATION_CALLS.store(0, Ordering::SeqCst); - let runtime = TestRuntime::new(BTreeMap::new()); - let jvm = create_test_jvm(runtime.clone()).await?; - jvm.register_class( - Box::new(ClassDefinitionImpl::from_class_proto( - FailingInitialization::as_proto(), - Box::new(runtime.clone()) as Box<_>, - )), - None, - ) - .await?; - - struct ReadFailingValue { - jvm: Jvm, - completed: Arc, - errors: Arc>>, - } - - #[async_trait::async_trait] - impl SpawnCallback for ReadFailingValue { - async fn call(&self) -> Result<()> { - self.jvm.attach_thread(None).await?; - if let Err(JavaError::JavaException(exception)) = self.jvm.get_static_field::("FailingInitialization", "value", "I").await { - self.errors.lock().unwrap().push(exception.class_definition().name()); - } - self.jvm.detach_thread()?; - self.completed.fetch_add(1, Ordering::SeqCst); - Ok(()) - } - } - - let completed = Arc::new(AtomicUsize::new(0)); - let errors = Arc::new(Mutex::new(alloc::vec::Vec::new())); - for _ in 0..2 { - runtime.spawn( - &jvm, - Box::new(ReadFailingValue { - jvm: jvm.clone(), - completed: completed.clone(), - errors: errors.clone(), - }), - ); - } - - for _ in 0..100 { - if completed.load(Ordering::SeqCst) == 2 { - break; - } - runtime.sleep(Duration::from_millis(5)).await; - } - - let mut errors = errors.lock().unwrap().clone(); - errors.sort(); - assert_eq!(completed.load(Ordering::SeqCst), 2); - assert_eq!(FAILING_INITIALIZATION_CALLS.load(Ordering::SeqCst), 1); - assert_eq!(errors, ["java/lang/ExceptionInInitializerError", "java/lang/NoClassDefFoundError"]); - - let result = jvm.get_static_field::("FailingInitialization", "value", "I").await; - let Err(JavaError::JavaException(exception)) = result else { - panic!("an erroneous class must remain erroneous"); - }; - assert!(jvm.is_instance(&*exception, "java/lang/NoClassDefFoundError")); - - Ok(()) -} diff --git a/test_data/ConcurrentClinit$Failing.class b/test_data/ConcurrentClinit$Failing.class new file mode 100644 index 0000000000000000000000000000000000000000..795add5f59a8440a14e1a8e44cc208a29284f759 GIT binary patch literal 783 zcmZ8fU2hUm5Iwg*cUhK#E!9eeqNOdM7C)bCOboFpTH8b#jJ{pqTDNW&vb)ref8bBh zKhWpahc=q%yMM?x^;{&7u$#=CJ9Ey?nVJ3d=i3hek8sz*6e0$qHe#3-NFT@}S#_ni zU)^mTsCFnYz2SIH_)s8HDmUXW5jSAjNWdn%*7e$b-&bB(b2Ut0?zMF2?(e?yl-~%Y zAF7Ukc%_*$Hd30F8Dd^YCmew+T)?b>IUAV?C3|mtCHaxq2S>S`7=`P{(u4&Ymyu;E zaFyx_L`zS~O#@d1^5ahG9;^8M9+f3Rn80@*RdJ2woosUwNDdXyAx9_*rCTmf`WSw;_mi!$R zgj?N!OS0=8w^MUv5U4=F7z#lFtNb4Tf>A=5-V|T{8H`kT%kdOEW0fD??=DFI7RXc;^#;o|NWi8{5dWiW9I31mNE&s1g7G literal 0 HcmV?d00001 diff --git a/test_data/ConcurrentClinit$FailingReader.class b/test_data/ConcurrentClinit$FailingReader.class new file mode 100644 index 0000000000000000000000000000000000000000..592b38eeee68dd4f6d67d77329d117031fa68102 GIT binary patch literal 809 zcmZ8f+iuf95Iq|w+1PO|xG4k*X@LTX3k}z&R!AjMpd}4bQx&Oin`8+tsf{*v`z?F{ z&jnf}9{2!03Nf1)1^lqHJ9FmjnVH>RzkmD$u!Uz9QZP8uHZsUEl#j%TXpV(HYQE_o z$wA1F-Sm7fe9mCh8wVzGFgYw6dDslKR^SgJrKBIW#yV}aEj&7m_M{j}MdVZAh5En_ zO%ze$DBHNCspTY8)Ow>c&z=T1EG%P%spBV@5)K|Dwxm{+uOb`XZ1-M zMOuWVevU z3z%q8^Z~LlPsY4u_Z#>p@bp&{zQEc1Ouf5n7r1tT+BsIwC_Rr76;UOEzO)8TwoUdW yAv=Wa68f4xPlvEB)(OqQ$hSGFc`BeuehLrqXu22in0ALEIV$f7EaDaM6xJW|6sj`- literal 0 HcmV?d00001 diff --git a/test_data/ConcurrentClinit$Successful.class b/test_data/ConcurrentClinit$Successful.class new file mode 100644 index 0000000000000000000000000000000000000000..7a13368db59bd3e1d53d46db0183a81983d5fbaf GIT binary patch literal 814 zcmZ8f%Wl&^6g}f4b>cV=@@i?-&=-&dXlQwCsF08aYD#G(QWe39i8GWbt{wSN9(#U* zALzQxq7_K6=ZDwsVr_$EI{# z+Qp9s)Xd`#wiu{$=~!WyjWN&ZnDR)B>uFqL$jX>$U>OZ%c$1;p*p8Xq(3kGPgMT*P zFmY3PoI2)CB&Z+D>r*IfOIPefM?K;1@t#AO^oBk1o<@`-qwIIj_~3uZ=z5WFiziay zs*^sis-20yBZ6?#3urcG$0fHdhX;WO7>szDSfI&k7)B~+;08rEpiw+We{ fE1%uev1m{eYn0Hijyt26$6bmBZILFSdocb2njhY8V1w|223ZmcwRx0#unvN2UQ^}&=;%B%a_}~Zl zQR1B{D)nLRe(pW@&ilvf8-Q)hnn*y?k+hIPnjwF|Z}@`G<@v%<<3hL*LwduLUbM-e zmFH>(GB9+Q7P??DlqxOh#z7!tRPj~WbTxKe5r${6e=K-Y1cblkUZf0GH;_e6N8Un@ zLgzct3W z;Zs4fT%W6vsp6DLzBLCy!y2WEL99MI^BV1IrX0L;4%HMJPtug7XCZ4tnwt-OdB-XAG(aiFN~< spuh@rtP+VeqOy*0ifCY6BqrL0B}}$;7E`2cf@Wy)G)(%fEM{PS0>ck|Qvd(} literal 0 HcmV?d00001 diff --git a/test_data/ConcurrentClinit.class b/test_data/ConcurrentClinit.class new file mode 100644 index 0000000000000000000000000000000000000000..03eb810f09bc6472d416bb4a3f2c5d28aa51101a GIT binary patch literal 1740 zcmZ`(-%}e^6#j1W#k%hsFUXeV@l2s! zESg$vxn7<%PRaNQXe1C(=Y)W9b(q0a4bE2kbAc7dCpSVj1J6; zO_z;sK^Ln9jvrV^qF3XXjpIl$>R~hry|sNjGaH`q3*u%<3Tjcpov?8-40p#hP0^qRyed&1e|z`TWvcv<5W8?Q>R6Nl?;Vx1AC z53W;o?>d27^|IBv7ie5k=>Lz~gyXVnD|?;FAR&ytZev8~>|Wk|ZYIt`23Ksn0Zu`} z_(7>H)J4W zT;H`Zi7AD)6~p-padf8#nvjG_0+~imBX45{*A+SrrmNwk>7REh>`zaNdjlv(@?eZ^ zIBw81M?DRa4ymKrYKh@Grd`kE>y;(r&k1Puv*NgHYj65y6U===6pnI2i??&mT4=Aa z!0Ml1O7_RUD4I3ta82V@3rG7ev?^5Ve$h<0k~?{r9hTqcpTHZQ>=Wc94BRMXQ=+H`wRa>f!R$|&GlB7w+9%6J0Z;Gh*#A}zVd*~jqL}_i|oh`galLsvL z7yO3bG0u{OhP&wJca}61&_zuz={QM3PGf=Vw#fCo$?d&`tN4h^^$BwL6gS}TTf{0# zsKYSEGQPwLzM|%9+{U*k<2zLFJv`jU8h*g%%vxjKb!HCmh+cnSgK@wOivKPDT&-cZ zKc literal 0 HcmV?d00001 diff --git a/test_data/ConcurrentClinit.txt b/test_data/ConcurrentClinit.txt new file mode 100644 index 00000000..3bd25ba7 --- /dev/null +++ b/test_data/ConcurrentClinit.txt @@ -0,0 +1,6 @@ +42 +42 +1 +java.lang.ExceptionInInitializerError +java.lang.NoClassDefFoundError +java.lang.NoClassDefFoundError diff --git a/test_data/src/ConcurrentClinit.java b/test_data/src/ConcurrentClinit.java new file mode 100644 index 00000000..e283a6ca --- /dev/null +++ b/test_data/src/ConcurrentClinit.java @@ -0,0 +1,120 @@ +public class ConcurrentClinit { + private static volatile boolean successfulOwnerStarted; + private static volatile boolean successfulWaiterStarted; + private static volatile boolean failingOwnerStarted; + private static volatile boolean failingWaiterStarted; + + private static class Successful { + static int initializationCount; + static int value; + + static { + initializationCount++; + successfulOwnerStarted = true; + while (!successfulWaiterStarted) { + Thread.yield(); + } + try { + Thread.sleep(10); + } catch (InterruptedException exception) { + throw new RuntimeException("interrupted"); + } + value = 42; + } + } + + private static class Failing { + static int value; + + static { + failingOwnerStarted = true; + while (!failingWaiterStarted) { + Thread.yield(); + } + try { + Thread.sleep(10); + } catch (InterruptedException exception) { + throw new RuntimeException("interrupted"); + } + if (failingOwnerStarted) { + throw new RuntimeException("initialization failed"); + } + } + } + + private static class SuccessfulReader implements Runnable { + private final boolean waiter; + int value; + + SuccessfulReader(boolean waiter) { + this.waiter = waiter; + } + + public void run() { + if (waiter) { + successfulWaiterStarted = true; + } + value = Successful.value; + } + } + + private static class FailingReader implements Runnable { + private final boolean waiter; + String errorClass; + + FailingReader(boolean waiter) { + this.waiter = waiter; + } + + public void run() { + if (waiter) { + failingWaiterStarted = true; + } + try { + int ignored = Failing.value; + } catch (Throwable throwable) { + errorClass = throwable.getClass().getName(); + } + } + } + + public static void main(String[] args) throws Exception { + SuccessfulReader successfulOwner = new SuccessfulReader(false); + Thread successfulOwnerThread = new Thread(successfulOwner); + successfulOwnerThread.start(); + while (!successfulOwnerStarted) { + Thread.yield(); + } + + SuccessfulReader successfulWaiter = new SuccessfulReader(true); + Thread successfulWaiterThread = new Thread(successfulWaiter); + successfulWaiterThread.start(); + successfulOwnerThread.join(); + successfulWaiterThread.join(); + + System.out.println(successfulOwner.value); + System.out.println(successfulWaiter.value); + System.out.println(Successful.initializationCount); + + FailingReader failingOwner = new FailingReader(false); + Thread failingOwnerThread = new Thread(failingOwner); + failingOwnerThread.start(); + while (!failingOwnerStarted) { + Thread.yield(); + } + + FailingReader failingWaiter = new FailingReader(true); + Thread failingWaiterThread = new Thread(failingWaiter); + failingWaiterThread.start(); + failingOwnerThread.join(); + failingWaiterThread.join(); + + System.out.println(failingOwner.errorClass); + System.out.println(failingWaiter.errorClass); + try { + int ignored = Failing.value; + } catch (Throwable throwable) { + System.out.println(throwable.getClass().getName()); + } + } +} From 39638dd13414c4b627377a12f79d31ba8032a2e4 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Fri, 17 Jul 2026 18:31:41 +0900 Subject: [PATCH 4/5] Separate classfile validation from JVM verification --- classfile/src/class.rs | 6 + classfile/src/lib.rs | 1 + classfile/src/validation.rs | 234 ++++++++++++++++++ classfile/tests/test.rs | 22 ++ java_runtime/src/classes/java/lang.rs | 3 +- .../src/classes/java/lang/verify_error.rs | 43 ++++ java_runtime/src/loader.rs | 1 + .../tests/classes/java/lang/test_class.rs | 9 + .../java/lang/test_cldc11_exceptions.rs | 1 + jvm_rust/src/class_definition.rs | 134 +--------- jvm_rust/src/error.rs | 14 ++ jvm_rust/src/lib.rs | 3 + jvm_rust/src/verifier.rs | 64 +++++ src/runtime.rs | 15 +- test_utils/src/lib.rs | 15 +- 15 files changed, 428 insertions(+), 137 deletions(-) create mode 100644 classfile/src/validation.rs create mode 100644 java_runtime/src/classes/java/lang/verify_error.rs create mode 100644 jvm_rust/src/error.rs create mode 100644 jvm_rust/src/verifier.rs diff --git a/classfile/src/class.rs b/classfile/src/class.rs index 6cf78075..059d6b70 100644 --- a/classfile/src/class.rs +++ b/classfile/src/class.rs @@ -11,6 +11,7 @@ use java_constants::ClassAccessFlags; use crate::{ ClassFileError, attribute::AttributeInfo, constant_pool::ConstantPoolItem, field::FieldInfo, interface::parse_interface, method::MethodInfo, + validation::validate_class, }; fn parse_this_class<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Arc> { @@ -109,7 +110,12 @@ impl ClassInfo { if result.major_version > 70 { return Err(ClassFileError::UnsupportedVersion(result.major_version)); } + validate_class(&result)?; Ok(result) } + + pub fn validate(&self) -> Result<(), ClassFileError> { + validate_class(self) + } } diff --git a/classfile/src/lib.rs b/classfile/src/lib.rs index ac250fe5..be214387 100644 --- a/classfile/src/lib.rs +++ b/classfile/src/lib.rs @@ -9,6 +9,7 @@ mod field; mod interface; mod method; mod opcode; +mod validation; pub use { attribute::{AttributeInfo, AttributeInfoCode}, diff --git a/classfile/src/validation.rs b/classfile/src/validation.rs new file mode 100644 index 00000000..b017351a --- /dev/null +++ b/classfile/src/validation.rs @@ -0,0 +1,234 @@ +use alloc::collections::BTreeMap; + +use java_constants::MethodAccessFlags; + +use crate::{AttributeInfo, ClassFileError, ClassInfo, ConstantPoolReference, constant_pool::ConstantPoolItem}; + +enum MemberKind { + Field, + Method, +} + +pub(crate) fn validate_class(class: &ClassInfo) -> Result<(), ClassFileError> { + if !is_internal_class_name(&class.this_class) + || class.super_class.as_ref().is_some_and(|name| !is_internal_class_name(name)) + || class.interfaces.iter().any(|name| !is_internal_class_name(name)) + || !validate_constant_pool(&class.constant_pool) + { + return Err(ClassFileError::InvalidFormat); + } + + for field in &class.fields { + if !is_field_descriptor(&field.descriptor) { + return Err(ClassFileError::InvalidFormat); + } + + let constant_values = field + .attributes + .iter() + .filter_map(|attribute| match attribute { + AttributeInfo::ConstantValue(value) => Some(value), + _ => None, + }) + .collect::>(); + if constant_values.len() > 1 + || constant_values.first().is_some_and(|value| { + !matches!( + (field.descriptor.as_str(), *value), + ("Z" | "B" | "C" | "S" | "I", ConstantPoolReference::Integer(_)) + | ("J", ConstantPoolReference::Long(_)) + | ("F", ConstantPoolReference::Float(_)) + | ("D", ConstantPoolReference::Double(_)) + | ("Ljava/lang/String;", ConstantPoolReference::String(_)) + ) + }) + { + return Err(ClassFileError::InvalidFormat); + } + } + + for method in &class.methods { + if !is_method_descriptor(&method.descriptor) { + return Err(ClassFileError::InvalidFormat); + } + + let code_attributes = method + .attributes + .iter() + .filter(|attribute| matches!(attribute, AttributeInfo::Code(_))) + .count(); + if method.access_flags.intersects(MethodAccessFlags::ABSTRACT | MethodAccessFlags::NATIVE) { + if code_attributes != 0 { + return Err(ClassFileError::InvalidFormat); + } + } else if code_attributes != 1 { + return Err(ClassFileError::InvalidFormat); + } + } + + Ok(()) +} + +fn validate_constant_pool(constant_pool: &BTreeMap) -> bool { + constant_pool.values().all(|item| match item { + ConstantPoolItem::Class { name_index } => constant_pool + .get(name_index) + .and_then(ConstantPoolItem::utf8) + .is_some_and(|name| is_class_constant_name(&name)), + ConstantPoolItem::String { string_index } => constant_pool.get(string_index).and_then(ConstantPoolItem::utf8).is_some(), + ConstantPoolItem::Fieldref { + class_index, + name_and_type_index, + } => validate_member_reference(constant_pool, *class_index, *name_and_type_index, MemberKind::Field), + ConstantPoolItem::Methodref { + class_index, + name_and_type_index, + } + | ConstantPoolItem::InterfaceMethodref { + class_index, + name_and_type_index, + } => validate_member_reference(constant_pool, *class_index, *name_and_type_index, MemberKind::Method), + ConstantPoolItem::NameAndType { + name_index, + descriptor_index, + } => { + let name = constant_pool.get(name_index).and_then(ConstantPoolItem::utf8); + let descriptor = constant_pool.get(descriptor_index).and_then(ConstantPoolItem::utf8); + name.is_some_and(|name| !name.is_empty()) + && descriptor.is_some_and(|descriptor| is_field_descriptor(&descriptor) || is_method_descriptor(&descriptor)) + } + _ => true, + }) +} + +fn validate_member_reference(constant_pool: &BTreeMap, class_index: u16, name_and_type_index: u16, kind: MemberKind) -> bool { + let class_name = constant_pool + .get(&class_index) + .and_then(ConstantPoolItem::class_name_index) + .and_then(|index| constant_pool.get(&index)) + .and_then(ConstantPoolItem::utf8); + let name_and_type = constant_pool.get(&name_and_type_index).and_then(ConstantPoolItem::name_and_type); + let Some((name_index, descriptor_index)) = name_and_type else { + return false; + }; + let name = constant_pool.get(&name_index).and_then(ConstantPoolItem::utf8); + let descriptor = constant_pool.get(&descriptor_index).and_then(ConstantPoolItem::utf8); + + class_name.is_some_and(|name| is_class_constant_name(&name)) + && name.is_some_and(|name| !name.is_empty()) + && descriptor.is_some_and(|descriptor| match kind { + MemberKind::Field => is_field_descriptor(&descriptor), + MemberKind::Method => is_method_descriptor(&descriptor), + }) +} + +fn is_internal_class_name(name: &str) -> bool { + !name.is_empty() && !name.starts_with('[') && !name.contains(['.', ';', '[']) +} + +fn is_class_constant_name(name: &str) -> bool { + is_internal_class_name(name) || array_dimensions(name).is_some() +} + +fn is_field_descriptor(descriptor: &str) -> bool { + let mut cursor = 0; + parse_field_type(descriptor.as_bytes(), &mut cursor) && cursor == descriptor.len() +} + +fn is_method_descriptor(descriptor: &str) -> bool { + let bytes = descriptor.as_bytes(); + if bytes.first() != Some(&b'(') { + return false; + } + + let mut cursor = 1; + while bytes.get(cursor).is_some_and(|byte| *byte != b')') { + if !parse_field_type(bytes, &mut cursor) { + return false; + } + } + if bytes.get(cursor) != Some(&b')') { + return false; + } + cursor += 1; + + if bytes.get(cursor) == Some(&b'V') { + cursor += 1; + } else if !parse_field_type(bytes, &mut cursor) { + return false; + } + + cursor == bytes.len() +} + +fn array_dimensions(descriptor: &str) -> Option { + let bytes = descriptor.as_bytes(); + let dimensions = bytes.iter().take_while(|byte| **byte == b'[').count(); + if dimensions == 0 || dimensions > u8::MAX as usize { + return None; + } + + let mut cursor = 0; + if parse_field_type(bytes, &mut cursor) && cursor == bytes.len() { + Some(dimensions) + } else { + None + } +} + +fn parse_field_type(bytes: &[u8], cursor: &mut usize) -> bool { + let mut dimensions = 0; + while bytes.get(*cursor) == Some(&b'[') { + dimensions += 1; + if dimensions > u8::MAX as usize { + return false; + } + *cursor += 1; + } + + match bytes.get(*cursor) { + Some(b'B' | b'C' | b'D' | b'F' | b'I' | b'J' | b'S' | b'Z') => { + *cursor += 1; + true + } + Some(b'L') => { + let name_start = *cursor + 1; + let Some(relative_end) = bytes[name_start..].iter().position(|byte| *byte == b';') else { + return false; + }; + let name_end = name_start + relative_end; + if name_end == name_start || bytes[name_start..name_end].iter().any(|byte| matches!(byte, b'.' | b'[' | b';')) { + return false; + } + *cursor = name_end + 1; + true + } + _ => false, + } +} + +#[cfg(test)] +mod tests { + use super::{array_dimensions, is_field_descriptor, is_method_descriptor}; + + #[test] + fn validates_field_and_method_descriptors() { + assert!(is_field_descriptor("Ljava/lang/String;")); + assert!(is_field_descriptor("[[I")); + assert!(!is_field_descriptor("V")); + assert!(!is_field_descriptor("[V")); + assert!(!is_field_descriptor("Igarbage")); + + assert!(is_method_descriptor("([Ljava/lang/String;I)V")); + assert!(!is_method_descriptor("(V)V")); + assert!(!is_method_descriptor("(I")); + assert!(!is_method_descriptor("()")); + } + + #[test] + fn counts_valid_array_dimensions() { + assert_eq!(array_dimensions("[[Ljava/lang/String;"), Some(2)); + assert_eq!(array_dimensions("java/lang/String"), None); + assert_eq!(array_dimensions("[V"), None); + } +} diff --git a/classfile/tests/test.rs b/classfile/tests/test.rs index bd78f10d..d01ac510 100644 --- a/classfile/tests/test.rs +++ b/classfile/tests/test.rs @@ -176,3 +176,25 @@ fn test_malformed_class_files_return_structured_errors() { invalid_constant_pool_type[44..46].copy_from_slice(&1u16.to_be_bytes()); assert_eq!(ClassInfo::parse(&invalid_constant_pool_type).err(), Some(ClassFileError::InvalidFormat)); } + +#[test] +fn test_class_info_validation_rejects_invalid_names_descriptors_and_code_layout() { + let hello = include_bytes!("../../test_data/Hello.class"); + + let mut invalid_name = ClassInfo::parse(hello).unwrap(); + invalid_name.this_class = "[I".to_string().into(); + assert_eq!(invalid_name.validate(), Err(ClassFileError::InvalidFormat)); + + let mut invalid_descriptor = ClassInfo::parse(hello).unwrap(); + invalid_descriptor.methods[0].descriptor = "(V)V".to_string().into(); + assert_eq!(invalid_descriptor.validate(), Err(ClassFileError::InvalidFormat)); + + let mut missing_code = ClassInfo::parse(hello).unwrap(); + missing_code.methods[0].attributes.clear(); + assert_eq!(missing_code.validate(), Err(ClassFileError::InvalidFormat)); +} + +#[test] +fn test_array_clone_method_owner_is_a_valid_class_constant() { + assert!(ClassInfo::parse(include_bytes!("../../test_data/Array.class")).is_ok()); +} diff --git a/java_runtime/src/classes/java/lang.rs b/java_runtime/src/classes/java/lang.rs index 9742f853..d7b8f6ae 100644 --- a/java_runtime/src/classes/java/lang.rs +++ b/java_runtime/src/classes/java/lang.rs @@ -54,6 +54,7 @@ mod throwable; mod unsatisfied_link_error; mod unsupported_class_version_error; mod unsupported_operation_exception; +mod verify_error; mod virtual_machine_error; pub use self::{ @@ -73,5 +74,5 @@ pub use self::{ runtime_exception::RuntimeException, security_exception::SecurityException, short::Short, string::String, string_buffer::StringBuffer, string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread, throwable::Throwable, unsatisfied_link_error::UnsatisfiedLinkError, unsupported_class_version_error::UnsupportedClassVersionError, - unsupported_operation_exception::UnsupportedOperationException, virtual_machine_error::VirtualMachineError, + unsupported_operation_exception::UnsupportedOperationException, verify_error::VerifyError, virtual_machine_error::VirtualMachineError, }; diff --git a/java_runtime/src/classes/java/lang/verify_error.rs b/java_runtime/src/classes/java/lang/verify_error.rs new file mode 100644 index 00000000..b1424668 --- /dev/null +++ b/java_runtime/src/classes/java/lang/verify_error.rs @@ -0,0 +1,43 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.VerifyError +pub struct VerifyError; + +impl VerifyError { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/VerifyError", + parent_class: Some("java/lang/LinkageError"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.VerifyError::({this:?})"); + + let _: () = jvm.invoke_special(&this, "java/lang/LinkageError", "", "()V", ()).await?; + + Ok(()) + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.VerifyError::({this:?}, {message:?})"); + + let _: () = jvm + .invoke_special(&this, "java/lang/LinkageError", "", "(Ljava/lang/String;)V", (message,)) + .await?; + + Ok(()) + } +} diff --git a/java_runtime/src/loader.rs b/java_runtime/src/loader.rs index be9a329d..6ae4be7a 100644 --- a/java_runtime/src/loader.rs +++ b/java_runtime/src/loader.rs @@ -92,6 +92,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::UnsupportedOperationException::as_proto(), crate::classes::java::lang::UnsupportedClassVersionError::as_proto(), crate::classes::java::lang::UnsatisfiedLinkError::as_proto(), + crate::classes::java::lang::VerifyError::as_proto(), crate::classes::java::lang::VirtualMachineError::as_proto(), crate::classes::java::net::JarURLConnection::as_proto(), crate::classes::java::net::MalformedURLException::as_proto(), diff --git a/java_runtime/tests/classes/java/lang/test_class.rs b/java_runtime/tests/classes/java/lang/test_class.rs index ae829c4d..80aecb4d 100644 --- a/java_runtime/tests/classes/java/lang/test_class.rs +++ b/java_runtime/tests/classes/java/lang/test_class.rs @@ -274,9 +274,18 @@ async fn test_define_class_translates_parser_errors_to_java_errors() -> Result<( let mut unsupported_version = include_bytes!("../../../../../test_data/Hello.class").to_vec(); unsupported_version[6..8].copy_from_slice(&71u16.to_be_bytes()); + let mut verification_error = include_bytes!("../../../../../test_data/MultiArray.class").to_vec(); + let multianewarray = [0x10, 0x0a, 0x10, 0x0a, 0x10, 0x0a, 0x10, 0x0a, 0x10, 0x0a, 0xc5, 0x00, 0x07, 0x05]; + let multianewarray_offset = verification_error + .windows(multianewarray.len()) + .position(|window| window == multianewarray) + .expect("MultiArray fixture must contain the expected multianewarray instruction"); + verification_error[multianewarray_offset + multianewarray.len() - 1] = 6; + for (data, expected_exception) in [ (vec![0, 1, 2, 3], "java/lang/ClassFormatError"), (unsupported_version, "java/lang/UnsupportedClassVersionError"), + (verification_error, "java/lang/VerifyError"), ] { let length = data.len() as i32; let mut bytes = jvm.instantiate_array("B", data.len()).await?; diff --git a/java_runtime/tests/classes/java/lang/test_cldc11_exceptions.rs b/java_runtime/tests/classes/java/lang/test_cldc11_exceptions.rs index ca95e7a5..1628ea0f 100644 --- a/java_runtime/tests/classes/java/lang/test_cldc11_exceptions.rs +++ b/java_runtime/tests/classes/java/lang/test_cldc11_exceptions.rs @@ -11,6 +11,7 @@ async fn test_cldc11_exception_and_error_hierarchy() -> Result<()> { ("java/lang/IllegalMonitorStateException", "java/lang/RuntimeException"), ("java/lang/IllegalThreadStateException", "java/lang/IllegalArgumentException"), ("java/lang/InstantiationException", "java/lang/Exception"), + ("java/lang/VerifyError", "java/lang/LinkageError"), ("java/lang/VirtualMachineError", "java/lang/Error"), ("java/lang/OutOfMemoryError", "java/lang/VirtualMachineError"), ("java/io/InterruptedIOException", "java/io/IOException"), diff --git a/jvm_rust/src/class_definition.rs b/jvm_rust/src/class_definition.rs index d248505c..0e411238 100644 --- a/jvm_rust/src/class_definition.rs +++ b/jvm_rust/src/class_definition.rs @@ -12,12 +12,12 @@ use core::{ use parking_lot::RwLock; -use classfile::{AttributeInfo, ClassFileError, ClassInfo, ConstantPoolReference, Opcode}; +use classfile::{AttributeInfo, ClassInfo, ConstantPoolReference}; use java_class_proto::JavaClassProto; use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; use jvm::{ClassDefinition, ClassInstance, Field, JavaType, JavaValue, Jvm, Method, Result}; -use crate::{class_instance::ClassInstanceImpl, field::FieldImpl, method::MethodImpl}; +use crate::{ClassDefinitionError, class_instance::ClassInstanceImpl, field::FieldImpl, method::MethodImpl, verifier}; struct ClassDefinitionInner { name: String, @@ -96,135 +96,9 @@ impl ClassDefinitionImpl { ) } - pub fn from_classfile(data: &[u8]) -> core::result::Result { + pub fn from_classfile(data: &[u8]) -> core::result::Result { let class = ClassInfo::parse(data)?; - - if class.this_class.is_empty() - || class.this_class.starts_with('[') - || class.super_class.as_ref().is_some_and(|name| name.is_empty() || name.starts_with('[')) - || class.interfaces.iter().any(|name| name.is_empty() || name.starts_with('[')) - { - return Err(ClassFileError::InvalidFormat); - } - for field in &class.fields { - let Some(r#type) = JavaType::try_parse(&field.descriptor) else { - return Err(ClassFileError::InvalidFormat); - }; - if matches!(r#type, JavaType::Void | JavaType::Method(_, _)) { - return Err(ClassFileError::InvalidFormat); - } - - let constant_values = field - .attributes - .iter() - .filter_map(|attribute| match attribute { - AttributeInfo::ConstantValue(value) => Some(value), - _ => None, - }) - .collect::>(); - if constant_values.len() > 1 - || constant_values.first().is_some_and(|value| { - !matches!( - (field.descriptor.as_str(), *value), - ("Z" | "B" | "C" | "S" | "I", ConstantPoolReference::Integer(_)) - | ("J", ConstantPoolReference::Long(_)) - | ("F", ConstantPoolReference::Float(_)) - | ("D", ConstantPoolReference::Double(_)) - | ("Ljava/lang/String;", ConstantPoolReference::String(_)) - ) - }) - { - return Err(ClassFileError::InvalidFormat); - } - } - for method in &class.methods { - if !matches!(JavaType::try_parse(&method.descriptor), Some(JavaType::Method(_, _))) { - return Err(ClassFileError::InvalidFormat); - } - - for attribute in &method.attributes { - let AttributeInfo::Code(code) = attribute else { - continue; - }; - for opcode in code.code.values() { - match opcode { - Opcode::Getfield(ConstantPoolReference::Field(reference)) - | Opcode::Getstatic(ConstantPoolReference::Field(reference)) - | Opcode::Putfield(ConstantPoolReference::Field(reference)) - | Opcode::Putstatic(ConstantPoolReference::Field(reference)) => { - let Some(r#type) = JavaType::try_parse(&reference.descriptor) else { - return Err(ClassFileError::InvalidFormat); - }; - if reference.class.is_empty() - || reference.class.starts_with('[') - || matches!(r#type, JavaType::Void | JavaType::Method(_, _)) - { - return Err(ClassFileError::InvalidFormat); - } - } - Opcode::Invokeinterface(ConstantPoolReference::InterfaceMethodref(reference), _, _) - | Opcode::Invokespecial(ConstantPoolReference::Method(reference)) - | Opcode::Invokestatic(ConstantPoolReference::Method(reference)) => { - if reference.class.is_empty() - || reference.class.starts_with('[') - || !matches!(JavaType::try_parse(&reference.descriptor), Some(JavaType::Method(_, _))) - { - return Err(ClassFileError::InvalidFormat); - } - } - Opcode::Invokevirtual(ConstantPoolReference::Method(reference)) => { - if reference.class.is_empty() - || (reference.class.starts_with('[') && !matches!(JavaType::try_parse(&reference.class), Some(JavaType::Array(_)))) - || !matches!(JavaType::try_parse(&reference.descriptor), Some(JavaType::Method(_, _))) - { - return Err(ClassFileError::InvalidFormat); - } - } - Opcode::Anewarray(ConstantPoolReference::Class(name)) - | Opcode::Checkcast(ConstantPoolReference::Class(name)) - | Opcode::Instanceof(ConstantPoolReference::Class(name)) - | Opcode::Ldc(ConstantPoolReference::Class(name)) - | Opcode::LdcW(ConstantPoolReference::Class(name)) - | Opcode::New(ConstantPoolReference::Class(name)) => { - if name.is_empty() - || (name.starts_with('[') && !matches!(JavaType::try_parse(name), Some(JavaType::Array(_)))) - || (!name.starts_with('[') && name.contains(['.', ';', '['])) - { - return Err(ClassFileError::InvalidFormat); - } - } - Opcode::Multianewarray(ConstantPoolReference::Class(name), dimensions) => { - let Some(mut r#type) = JavaType::try_parse(name) else { - return Err(ClassFileError::InvalidFormat); - }; - let mut available_dimensions = 0; - while let JavaType::Array(element) = r#type { - available_dimensions += 1; - r#type = *element; - } - if available_dimensions < *dimensions as usize { - return Err(ClassFileError::InvalidFormat); - } - } - Opcode::Invokedynamic(_) => return Err(ClassFileError::InvalidFormat), - _ => {} - } - } - } - - let code_attributes = method - .attributes - .iter() - .filter(|attribute| matches!(attribute, AttributeInfo::Code(_))) - .count(); - if method.access_flags.intersects(MethodAccessFlags::ABSTRACT | MethodAccessFlags::NATIVE) { - if code_attributes != 0 { - return Err(ClassFileError::InvalidFormat); - } - } else if code_attributes != 1 { - return Err(ClassFileError::InvalidFormat); - } - } + verifier::verify(&class)?; let mut constant_values = Vec::new(); let fields = class diff --git a/jvm_rust/src/error.rs b/jvm_rust/src/error.rs new file mode 100644 index 00000000..b0e9996e --- /dev/null +++ b/jvm_rust/src/error.rs @@ -0,0 +1,14 @@ +use classfile::ClassFileError; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ClassDefinitionError { + ClassFile(ClassFileError), + Verification, + UnsupportedFeature(&'static str), +} + +impl From for ClassDefinitionError { + fn from(error: ClassFileError) -> Self { + Self::ClassFile(error) + } +} diff --git a/jvm_rust/src/lib.rs b/jvm_rust/src/lib.rs index 62fed997..28506e94 100644 --- a/jvm_rust/src/lib.rs +++ b/jvm_rust/src/lib.rs @@ -5,14 +5,17 @@ mod array_class_definition; mod array_class_instance; mod class_definition; mod class_instance; +mod error; mod field; mod interpreter; mod method; mod stack_frame; +mod verifier; pub use self::{ array_class_definition::ArrayClassDefinitionImpl, class_definition::ClassDefinitionImpl, + error::ClassDefinitionError, field::FieldImpl, method::{MethodBody, MethodImpl}, }; diff --git a/jvm_rust/src/verifier.rs b/jvm_rust/src/verifier.rs new file mode 100644 index 00000000..01509595 --- /dev/null +++ b/jvm_rust/src/verifier.rs @@ -0,0 +1,64 @@ +use classfile::{AttributeInfo, ClassInfo, ConstantPoolReference, Opcode}; +use jvm::JavaType; + +use crate::ClassDefinitionError; + +pub(crate) fn verify(class: &ClassInfo) -> Result<(), ClassDefinitionError> { + for method in &class.methods { + for attribute in &method.attributes { + let AttributeInfo::Code(code) = attribute else { + continue; + }; + for opcode in code.code.values() { + match opcode { + Opcode::Multianewarray(ConstantPoolReference::Class(name), dimensions) => { + let Some(mut r#type) = JavaType::try_parse(name) else { + return Err(ClassDefinitionError::Verification); + }; + let mut available_dimensions = 0; + while let JavaType::Array(element) = r#type { + available_dimensions += 1; + r#type = *element; + } + if available_dimensions < *dimensions as usize { + return Err(ClassDefinitionError::Verification); + } + } + Opcode::Invokedynamic(_) => return Err(ClassDefinitionError::UnsupportedFeature("invokedynamic")), + _ => {} + } + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use classfile::{AttributeInfo, ClassInfo, Opcode}; + + use crate::{ClassDefinitionError, verifier::verify}; + + #[test] + fn rejects_multianewarray_dimensions_larger_than_the_array_type() { + let mut class = ClassInfo::parse(include_bytes!("../../test_data/MultiArray.class")).unwrap(); + let mut changed = false; + for method in &mut class.methods { + for attribute in &mut method.attributes { + let AttributeInfo::Code(code) = attribute else { + continue; + }; + for opcode in code.code.values_mut() { + if let Opcode::Multianewarray(_, dimensions) = opcode { + *dimensions = u8::MAX; + changed = true; + } + } + } + } + + assert!(changed); + assert_eq!(verify(&class), Err(ClassDefinitionError::Verification)); + } +} diff --git a/src/runtime.rs b/src/runtime.rs index 95bf6893..6de345f7 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -14,7 +14,7 @@ use std::{ use java_runtime::{File, FileDescriptorId, FileStat, FileType, IOError, IOResult, RT_RUSTJAR, Runtime, SpawnCallback, get_runtime_class_proto}; use jvm::{ClassDefinition, Jvm}; -use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionImpl, ClassFileError}; +use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionError, ClassDefinitionImpl, ClassFileError}; use self::io::{FileImpl, InputStreamFile, WriteStreamFile}; @@ -184,13 +184,22 @@ where async fn define_class(&self, jvm: &Jvm, data: &[u8]) -> jvm::Result> { match ClassDefinitionImpl::from_classfile(data) { Ok(class) => Ok(Box::new(class)), - Err(ClassFileError::InvalidFormat) => Err(jvm.exception("java/lang/ClassFormatError", "Invalid class file").await), - Err(ClassFileError::UnsupportedVersion(version)) => Err(jvm + Err(ClassDefinitionError::ClassFile(ClassFileError::InvalidFormat)) => { + Err(jvm.exception("java/lang/ClassFormatError", "Invalid class file").await) + } + Err(ClassDefinitionError::ClassFile(ClassFileError::UnsupportedVersion(version))) => Err(jvm .exception( "java/lang/UnsupportedClassVersionError", &format!("Unsupported class file version {version}"), ) .await), + Err(ClassDefinitionError::Verification) => Err(jvm.exception("java/lang/VerifyError", "Bytecode verification failed").await), + Err(ClassDefinitionError::UnsupportedFeature(feature)) => Err(jvm + .exception( + "java/lang/UnsupportedOperationException", + &format!("Unsupported class file feature: {feature}"), + ) + .await), } } diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs index f37b8a5f..26e44b42 100644 --- a/test_utils/src/lib.rs +++ b/test_utils/src/lib.rs @@ -12,7 +12,7 @@ use std::{ }; use jvm::{ClassDefinition, Jvm, Result}; -use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionImpl, ClassFileError}; +use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionError, ClassDefinitionImpl, ClassFileError}; use java_runtime::{ File, FileDescriptorId, FileSize, FileStat, FileType, IOError, IOResult, RT_RUSTJAR, Runtime, SpawnCallback, get_bootstrap_class_loader, @@ -163,13 +163,22 @@ impl Runtime for TestRuntime { async fn define_class(&self, jvm: &Jvm, data: &[u8]) -> jvm::Result> { match ClassDefinitionImpl::from_classfile(data) { Ok(class) => Ok(Box::new(class)), - Err(ClassFileError::InvalidFormat) => Err(jvm.exception("java/lang/ClassFormatError", "Invalid class file").await), - Err(ClassFileError::UnsupportedVersion(version)) => Err(jvm + Err(ClassDefinitionError::ClassFile(ClassFileError::InvalidFormat)) => { + Err(jvm.exception("java/lang/ClassFormatError", "Invalid class file").await) + } + Err(ClassDefinitionError::ClassFile(ClassFileError::UnsupportedVersion(version))) => Err(jvm .exception( "java/lang/UnsupportedClassVersionError", &format!("Unsupported class file version {version}"), ) .await), + Err(ClassDefinitionError::Verification) => Err(jvm.exception("java/lang/VerifyError", "Bytecode verification failed").await), + Err(ClassDefinitionError::UnsupportedFeature(feature)) => Err(jvm + .exception( + "java/lang/UnsupportedOperationException", + &format!("Unsupported class file feature: {feature}"), + ) + .await), } } From d8670953810a4fb4f6f6e298f50d933c409d3e1c Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Fri, 17 Jul 2026 18:38:43 +0900 Subject: [PATCH 5/5] Remove ClassFileError re-export --- Cargo.lock | 2 ++ Cargo.toml | 1 + jvm_rust/src/lib.rs | 1 - src/runtime.rs | 3 ++- test_utils/Cargo.toml | 1 + test_utils/src/lib.rs | 3 ++- 6 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e3b84554..8ec986cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -573,6 +573,7 @@ dependencies = [ "anyhow", "async-trait", "bytemuck", + "classfile", "java_class_proto", "java_runtime", "jvm", @@ -643,6 +644,7 @@ name = "test_utils" version = "0.0.1" dependencies = [ "async-trait", + "classfile", "java_runtime", "jvm", "jvm_rust", diff --git a/Cargo.toml b/Cargo.toml index 5840955a..4399f92a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ tracing-subscriber = { version = "^0.3", features = ["env-filter"] } jvm = { workspace = true } jvm_rust = { workspace = true } +classfile = { workspace = true } java_class_proto = { workspace = true } java_runtime = { workspace = true } diff --git a/jvm_rust/src/lib.rs b/jvm_rust/src/lib.rs index 28506e94..7fab6b66 100644 --- a/jvm_rust/src/lib.rs +++ b/jvm_rust/src/lib.rs @@ -19,4 +19,3 @@ pub use self::{ field::FieldImpl, method::{MethodBody, MethodImpl}, }; -pub use classfile::ClassFileError; diff --git a/src/runtime.rs b/src/runtime.rs index 6de345f7..79e45d39 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -12,9 +12,10 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; +use classfile::ClassFileError; use java_runtime::{File, FileDescriptorId, FileStat, FileType, IOError, IOResult, RT_RUSTJAR, Runtime, SpawnCallback, get_runtime_class_proto}; use jvm::{ClassDefinition, Jvm}; -use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionError, ClassDefinitionImpl, ClassFileError}; +use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionError, ClassDefinitionImpl}; use self::io::{FileImpl, InputStreamFile, WriteStreamFile}; diff --git a/test_utils/Cargo.toml b/test_utils/Cargo.toml index 11a2d435..76b5cc74 100644 --- a/test_utils/Cargo.toml +++ b/test_utils/Cargo.toml @@ -9,6 +9,7 @@ async-trait = { workspace = true } tracing = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "time"] } +classfile = { workspace = true } jvm = { workspace = true } jvm_rust = { workspace = true } java_runtime = { workspace = true } diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs index 26e44b42..7656aa89 100644 --- a/test_utils/src/lib.rs +++ b/test_utils/src/lib.rs @@ -11,8 +11,9 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; +use classfile::ClassFileError; use jvm::{ClassDefinition, Jvm, Result}; -use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionError, ClassDefinitionImpl, ClassFileError}; +use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionError, ClassDefinitionImpl}; use java_runtime::{ File, FileDescriptorId, FileSize, FileStat, FileType, IOError, IOResult, RT_RUSTJAR, Runtime, SpawnCallback, get_bootstrap_class_loader,