Skip to content

Commit 39638dd

Browse files
committed
Separate classfile validation from JVM verification
1 parent 8836e41 commit 39638dd

15 files changed

Lines changed: 428 additions & 137 deletions

File tree

classfile/src/class.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use java_constants::ClassAccessFlags;
1111

1212
use crate::{
1313
ClassFileError, attribute::AttributeInfo, constant_pool::ConstantPoolItem, field::FieldInfo, interface::parse_interface, method::MethodInfo,
14+
validation::validate_class,
1415
};
1516

1617
fn parse_this_class<'a>(data: &'a [u8], constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> IResult<&'a [u8], Arc<String>> {
@@ -109,7 +110,12 @@ impl ClassInfo {
109110
if result.major_version > 70 {
110111
return Err(ClassFileError::UnsupportedVersion(result.major_version));
111112
}
113+
validate_class(&result)?;
112114

113115
Ok(result)
114116
}
117+
118+
pub fn validate(&self) -> Result<(), ClassFileError> {
119+
validate_class(self)
120+
}
115121
}

classfile/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod field;
99
mod interface;
1010
mod method;
1111
mod opcode;
12+
mod validation;
1213

1314
pub use {
1415
attribute::{AttributeInfo, AttributeInfoCode},

classfile/src/validation.rs

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
use alloc::collections::BTreeMap;
2+
3+
use java_constants::MethodAccessFlags;
4+
5+
use crate::{AttributeInfo, ClassFileError, ClassInfo, ConstantPoolReference, constant_pool::ConstantPoolItem};
6+
7+
enum MemberKind {
8+
Field,
9+
Method,
10+
}
11+
12+
pub(crate) fn validate_class(class: &ClassInfo) -> Result<(), ClassFileError> {
13+
if !is_internal_class_name(&class.this_class)
14+
|| class.super_class.as_ref().is_some_and(|name| !is_internal_class_name(name))
15+
|| class.interfaces.iter().any(|name| !is_internal_class_name(name))
16+
|| !validate_constant_pool(&class.constant_pool)
17+
{
18+
return Err(ClassFileError::InvalidFormat);
19+
}
20+
21+
for field in &class.fields {
22+
if !is_field_descriptor(&field.descriptor) {
23+
return Err(ClassFileError::InvalidFormat);
24+
}
25+
26+
let constant_values = field
27+
.attributes
28+
.iter()
29+
.filter_map(|attribute| match attribute {
30+
AttributeInfo::ConstantValue(value) => Some(value),
31+
_ => None,
32+
})
33+
.collect::<alloc::vec::Vec<_>>();
34+
if constant_values.len() > 1
35+
|| constant_values.first().is_some_and(|value| {
36+
!matches!(
37+
(field.descriptor.as_str(), *value),
38+
("Z" | "B" | "C" | "S" | "I", ConstantPoolReference::Integer(_))
39+
| ("J", ConstantPoolReference::Long(_))
40+
| ("F", ConstantPoolReference::Float(_))
41+
| ("D", ConstantPoolReference::Double(_))
42+
| ("Ljava/lang/String;", ConstantPoolReference::String(_))
43+
)
44+
})
45+
{
46+
return Err(ClassFileError::InvalidFormat);
47+
}
48+
}
49+
50+
for method in &class.methods {
51+
if !is_method_descriptor(&method.descriptor) {
52+
return Err(ClassFileError::InvalidFormat);
53+
}
54+
55+
let code_attributes = method
56+
.attributes
57+
.iter()
58+
.filter(|attribute| matches!(attribute, AttributeInfo::Code(_)))
59+
.count();
60+
if method.access_flags.intersects(MethodAccessFlags::ABSTRACT | MethodAccessFlags::NATIVE) {
61+
if code_attributes != 0 {
62+
return Err(ClassFileError::InvalidFormat);
63+
}
64+
} else if code_attributes != 1 {
65+
return Err(ClassFileError::InvalidFormat);
66+
}
67+
}
68+
69+
Ok(())
70+
}
71+
72+
fn validate_constant_pool(constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> bool {
73+
constant_pool.values().all(|item| match item {
74+
ConstantPoolItem::Class { name_index } => constant_pool
75+
.get(name_index)
76+
.and_then(ConstantPoolItem::utf8)
77+
.is_some_and(|name| is_class_constant_name(&name)),
78+
ConstantPoolItem::String { string_index } => constant_pool.get(string_index).and_then(ConstantPoolItem::utf8).is_some(),
79+
ConstantPoolItem::Fieldref {
80+
class_index,
81+
name_and_type_index,
82+
} => validate_member_reference(constant_pool, *class_index, *name_and_type_index, MemberKind::Field),
83+
ConstantPoolItem::Methodref {
84+
class_index,
85+
name_and_type_index,
86+
}
87+
| ConstantPoolItem::InterfaceMethodref {
88+
class_index,
89+
name_and_type_index,
90+
} => validate_member_reference(constant_pool, *class_index, *name_and_type_index, MemberKind::Method),
91+
ConstantPoolItem::NameAndType {
92+
name_index,
93+
descriptor_index,
94+
} => {
95+
let name = constant_pool.get(name_index).and_then(ConstantPoolItem::utf8);
96+
let descriptor = constant_pool.get(descriptor_index).and_then(ConstantPoolItem::utf8);
97+
name.is_some_and(|name| !name.is_empty())
98+
&& descriptor.is_some_and(|descriptor| is_field_descriptor(&descriptor) || is_method_descriptor(&descriptor))
99+
}
100+
_ => true,
101+
})
102+
}
103+
104+
fn validate_member_reference(constant_pool: &BTreeMap<u16, ConstantPoolItem>, class_index: u16, name_and_type_index: u16, kind: MemberKind) -> bool {
105+
let class_name = constant_pool
106+
.get(&class_index)
107+
.and_then(ConstantPoolItem::class_name_index)
108+
.and_then(|index| constant_pool.get(&index))
109+
.and_then(ConstantPoolItem::utf8);
110+
let name_and_type = constant_pool.get(&name_and_type_index).and_then(ConstantPoolItem::name_and_type);
111+
let Some((name_index, descriptor_index)) = name_and_type else {
112+
return false;
113+
};
114+
let name = constant_pool.get(&name_index).and_then(ConstantPoolItem::utf8);
115+
let descriptor = constant_pool.get(&descriptor_index).and_then(ConstantPoolItem::utf8);
116+
117+
class_name.is_some_and(|name| is_class_constant_name(&name))
118+
&& name.is_some_and(|name| !name.is_empty())
119+
&& descriptor.is_some_and(|descriptor| match kind {
120+
MemberKind::Field => is_field_descriptor(&descriptor),
121+
MemberKind::Method => is_method_descriptor(&descriptor),
122+
})
123+
}
124+
125+
fn is_internal_class_name(name: &str) -> bool {
126+
!name.is_empty() && !name.starts_with('[') && !name.contains(['.', ';', '['])
127+
}
128+
129+
fn is_class_constant_name(name: &str) -> bool {
130+
is_internal_class_name(name) || array_dimensions(name).is_some()
131+
}
132+
133+
fn is_field_descriptor(descriptor: &str) -> bool {
134+
let mut cursor = 0;
135+
parse_field_type(descriptor.as_bytes(), &mut cursor) && cursor == descriptor.len()
136+
}
137+
138+
fn is_method_descriptor(descriptor: &str) -> bool {
139+
let bytes = descriptor.as_bytes();
140+
if bytes.first() != Some(&b'(') {
141+
return false;
142+
}
143+
144+
let mut cursor = 1;
145+
while bytes.get(cursor).is_some_and(|byte| *byte != b')') {
146+
if !parse_field_type(bytes, &mut cursor) {
147+
return false;
148+
}
149+
}
150+
if bytes.get(cursor) != Some(&b')') {
151+
return false;
152+
}
153+
cursor += 1;
154+
155+
if bytes.get(cursor) == Some(&b'V') {
156+
cursor += 1;
157+
} else if !parse_field_type(bytes, &mut cursor) {
158+
return false;
159+
}
160+
161+
cursor == bytes.len()
162+
}
163+
164+
fn array_dimensions(descriptor: &str) -> Option<usize> {
165+
let bytes = descriptor.as_bytes();
166+
let dimensions = bytes.iter().take_while(|byte| **byte == b'[').count();
167+
if dimensions == 0 || dimensions > u8::MAX as usize {
168+
return None;
169+
}
170+
171+
let mut cursor = 0;
172+
if parse_field_type(bytes, &mut cursor) && cursor == bytes.len() {
173+
Some(dimensions)
174+
} else {
175+
None
176+
}
177+
}
178+
179+
fn parse_field_type(bytes: &[u8], cursor: &mut usize) -> bool {
180+
let mut dimensions = 0;
181+
while bytes.get(*cursor) == Some(&b'[') {
182+
dimensions += 1;
183+
if dimensions > u8::MAX as usize {
184+
return false;
185+
}
186+
*cursor += 1;
187+
}
188+
189+
match bytes.get(*cursor) {
190+
Some(b'B' | b'C' | b'D' | b'F' | b'I' | b'J' | b'S' | b'Z') => {
191+
*cursor += 1;
192+
true
193+
}
194+
Some(b'L') => {
195+
let name_start = *cursor + 1;
196+
let Some(relative_end) = bytes[name_start..].iter().position(|byte| *byte == b';') else {
197+
return false;
198+
};
199+
let name_end = name_start + relative_end;
200+
if name_end == name_start || bytes[name_start..name_end].iter().any(|byte| matches!(byte, b'.' | b'[' | b';')) {
201+
return false;
202+
}
203+
*cursor = name_end + 1;
204+
true
205+
}
206+
_ => false,
207+
}
208+
}
209+
210+
#[cfg(test)]
211+
mod tests {
212+
use super::{array_dimensions, is_field_descriptor, is_method_descriptor};
213+
214+
#[test]
215+
fn validates_field_and_method_descriptors() {
216+
assert!(is_field_descriptor("Ljava/lang/String;"));
217+
assert!(is_field_descriptor("[[I"));
218+
assert!(!is_field_descriptor("V"));
219+
assert!(!is_field_descriptor("[V"));
220+
assert!(!is_field_descriptor("Igarbage"));
221+
222+
assert!(is_method_descriptor("([Ljava/lang/String;I)V"));
223+
assert!(!is_method_descriptor("(V)V"));
224+
assert!(!is_method_descriptor("(I"));
225+
assert!(!is_method_descriptor("()"));
226+
}
227+
228+
#[test]
229+
fn counts_valid_array_dimensions() {
230+
assert_eq!(array_dimensions("[[Ljava/lang/String;"), Some(2));
231+
assert_eq!(array_dimensions("java/lang/String"), None);
232+
assert_eq!(array_dimensions("[V"), None);
233+
}
234+
}

classfile/tests/test.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,25 @@ fn test_malformed_class_files_return_structured_errors() {
176176
invalid_constant_pool_type[44..46].copy_from_slice(&1u16.to_be_bytes());
177177
assert_eq!(ClassInfo::parse(&invalid_constant_pool_type).err(), Some(ClassFileError::InvalidFormat));
178178
}
179+
180+
#[test]
181+
fn test_class_info_validation_rejects_invalid_names_descriptors_and_code_layout() {
182+
let hello = include_bytes!("../../test_data/Hello.class");
183+
184+
let mut invalid_name = ClassInfo::parse(hello).unwrap();
185+
invalid_name.this_class = "[I".to_string().into();
186+
assert_eq!(invalid_name.validate(), Err(ClassFileError::InvalidFormat));
187+
188+
let mut invalid_descriptor = ClassInfo::parse(hello).unwrap();
189+
invalid_descriptor.methods[0].descriptor = "(V)V".to_string().into();
190+
assert_eq!(invalid_descriptor.validate(), Err(ClassFileError::InvalidFormat));
191+
192+
let mut missing_code = ClassInfo::parse(hello).unwrap();
193+
missing_code.methods[0].attributes.clear();
194+
assert_eq!(missing_code.validate(), Err(ClassFileError::InvalidFormat));
195+
}
196+
197+
#[test]
198+
fn test_array_clone_method_owner_is_a_valid_class_constant() {
199+
assert!(ClassInfo::parse(include_bytes!("../../test_data/Array.class")).is_ok());
200+
}

java_runtime/src/classes/java/lang.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ mod throwable;
5454
mod unsatisfied_link_error;
5555
mod unsupported_class_version_error;
5656
mod unsupported_operation_exception;
57+
mod verify_error;
5758
mod virtual_machine_error;
5859

5960
pub use self::{
@@ -73,5 +74,5 @@ pub use self::{
7374
runtime_exception::RuntimeException, security_exception::SecurityException, short::Short, string::String, string_buffer::StringBuffer,
7475
string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread, throwable::Throwable,
7576
unsatisfied_link_error::UnsatisfiedLinkError, unsupported_class_version_error::UnsupportedClassVersionError,
76-
unsupported_operation_exception::UnsupportedOperationException, virtual_machine_error::VirtualMachineError,
77+
unsupported_operation_exception::UnsupportedOperationException, verify_error::VerifyError, virtual_machine_error::VirtualMachineError,
7778
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use alloc::vec;
2+
3+
use java_class_proto::JavaMethodProto;
4+
use jvm::{ClassInstanceRef, Jvm, Result};
5+
6+
use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String};
7+
8+
// class java.lang.VerifyError
9+
pub struct VerifyError;
10+
11+
impl VerifyError {
12+
pub fn as_proto() -> RuntimeClassProto {
13+
RuntimeClassProto {
14+
name: "java/lang/VerifyError",
15+
parent_class: Some("java/lang/LinkageError"),
16+
interfaces: vec![],
17+
methods: vec![
18+
JavaMethodProto::new("<init>", "()V", Self::init, Default::default()),
19+
JavaMethodProto::new("<init>", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()),
20+
],
21+
fields: vec![],
22+
access_flags: Default::default(),
23+
}
24+
}
25+
26+
async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef<Self>) -> Result<()> {
27+
tracing::debug!("java.lang.VerifyError::<init>({this:?})");
28+
29+
let _: () = jvm.invoke_special(&this, "java/lang/LinkageError", "<init>", "()V", ()).await?;
30+
31+
Ok(())
32+
}
33+
34+
async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef<Self>, message: ClassInstanceRef<String>) -> Result<()> {
35+
tracing::debug!("java.lang.VerifyError::<init>({this:?}, {message:?})");
36+
37+
let _: () = jvm
38+
.invoke_special(&this, "java/lang/LinkageError", "<init>", "(Ljava/lang/String;)V", (message,))
39+
.await?;
40+
41+
Ok(())
42+
}
43+
}

java_runtime/src/loader.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option<RuntimeClassProto> {
9292
crate::classes::java::lang::UnsupportedOperationException::as_proto(),
9393
crate::classes::java::lang::UnsupportedClassVersionError::as_proto(),
9494
crate::classes::java::lang::UnsatisfiedLinkError::as_proto(),
95+
crate::classes::java::lang::VerifyError::as_proto(),
9596
crate::classes::java::lang::VirtualMachineError::as_proto(),
9697
crate::classes::java::net::JarURLConnection::as_proto(),
9798
crate::classes::java::net::MalformedURLException::as_proto(),

java_runtime/tests/classes/java/lang/test_class.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,18 @@ async fn test_define_class_translates_parser_errors_to_java_errors() -> Result<(
274274
let mut unsupported_version = include_bytes!("../../../../../test_data/Hello.class").to_vec();
275275
unsupported_version[6..8].copy_from_slice(&71u16.to_be_bytes());
276276

277+
let mut verification_error = include_bytes!("../../../../../test_data/MultiArray.class").to_vec();
278+
let multianewarray = [0x10, 0x0a, 0x10, 0x0a, 0x10, 0x0a, 0x10, 0x0a, 0x10, 0x0a, 0xc5, 0x00, 0x07, 0x05];
279+
let multianewarray_offset = verification_error
280+
.windows(multianewarray.len())
281+
.position(|window| window == multianewarray)
282+
.expect("MultiArray fixture must contain the expected multianewarray instruction");
283+
verification_error[multianewarray_offset + multianewarray.len() - 1] = 6;
284+
277285
for (data, expected_exception) in [
278286
(vec![0, 1, 2, 3], "java/lang/ClassFormatError"),
279287
(unsupported_version, "java/lang/UnsupportedClassVersionError"),
288+
(verification_error, "java/lang/VerifyError"),
280289
] {
281290
let length = data.len() as i32;
282291
let mut bytes = jvm.instantiate_array("B", data.len()).await?;

java_runtime/tests/classes/java/lang/test_cldc11_exceptions.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ async fn test_cldc11_exception_and_error_hierarchy() -> Result<()> {
1111
("java/lang/IllegalMonitorStateException", "java/lang/RuntimeException"),
1212
("java/lang/IllegalThreadStateException", "java/lang/IllegalArgumentException"),
1313
("java/lang/InstantiationException", "java/lang/Exception"),
14+
("java/lang/VerifyError", "java/lang/LinkageError"),
1415
("java/lang/VirtualMachineError", "java/lang/Error"),
1516
("java/lang/OutOfMemoryError", "java/lang/VirtualMachineError"),
1617
("java/io/InterruptedIOException", "java/io/IOException"),

0 commit comments

Comments
 (0)