Skip to content

Commit 822504b

Browse files
authored
Harden JVM runtime correctness (#180)
* Harden JVM runtime correctness * Address classfile review findings * Move class initialization tests to Java fixture * Separate classfile validation from JVM verification * Remove ClassFileError re-export
1 parent af4f6f8 commit 822504b

69 files changed

Lines changed: 2127 additions & 356 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,8 @@
2222
- `classfile/` - Class file parser
2323
- `java_class_proto/` - Java class prototypes
2424
- `test_utils/` - Shared test utilities
25+
26+
## Testing Boundaries
27+
- Keep `java_runtime/tests/classes` limited to Java standard library class and API behavior.
28+
- 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`.
29+
- Do not place JVM core behavior tests in the `java_runtime` standard library test tree.

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,20 @@ async-trait = { workspace = true }
4242
bytemuck = { workspace = true }
4343

4444
anyhow = { workspace = true }
45+
tracing = { workspace = true }
4546
tracing-subscriber = { version = "^0.3", features = ["env-filter"] }
4647

4748
jvm = { workspace = true }
4849
jvm_rust = { workspace = true }
50+
classfile = { workspace = true }
4951
java_class_proto = { workspace = true }
5052
java_runtime = { workspace = true }
5153

5254
[dev-dependencies]
5355
test_utils = { workspace = true }
5456

5557
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
56-
tokio = { workspace = true, features = ["rt-multi-thread"] }
58+
tokio = { workspace = true, features = ["rt-multi-thread", "time"] }
5759

5860
[target.'cfg(target_arch = "wasm32")'.dependencies]
59-
tokio = { workspace = true, features = ["rt"] }
61+
tokio = { workspace = true, features = ["rt", "time"] }

classfile/src/attribute.rs

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,20 @@ pub struct CodeAttributeExceptionTable {
1919

2020
impl CodeAttributeExceptionTable {
2121
pub fn parse<'a>(data: &'a [u8], constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> IResult<&'a [u8], Self> {
22-
map((be_u16, be_u16, be_u16, be_u16), |(start_pc, end_pc, handler_pc, catch_type)| {
22+
map_res((be_u16, be_u16, be_u16, be_u16), |(start_pc, end_pc, handler_pc, catch_type)| {
2323
let catch_type = if catch_type != 0 {
24-
let index = constant_pool.get(&catch_type).unwrap().class_name_index();
25-
Some(constant_pool.get(&index).unwrap().utf8())
24+
let index = constant_pool.get(&catch_type).and_then(ConstantPoolItem::class_name_index).ok_or(())?;
25+
Some(constant_pool.get(&index).and_then(ConstantPoolItem::utf8).ok_or(())?)
2626
} else {
2727
None
2828
};
2929

30-
Self {
30+
Ok::<_, ()>(Self {
3131
start_pc,
3232
end_pc,
3333
handler_pc,
3434
catch_type,
35-
}
35+
})
3636
})
3737
.parse(data)
3838
}
@@ -52,7 +52,7 @@ impl AttributeInfoCode {
5252
(
5353
be_u16,
5454
be_u16,
55-
map(flat_map(be_u32, take), |x: &[u8]| Self::parse_code(x, constant_pool)),
55+
map_res(flat_map(be_u32, take), |x: &[u8]| Self::parse_code(x, constant_pool)),
5656
length_count(be_u16, |x| CodeAttributeExceptionTable::parse(x, constant_pool)),
5757
length_count(be_u16, |x| AttributeInfo::parse(x, constant_pool)),
5858
),
@@ -67,22 +67,21 @@ impl AttributeInfoCode {
6767
.parse(data)
6868
}
6969

70-
fn parse_code(code: &[u8], constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> BTreeMap<u32, Opcode> {
70+
fn parse_code(code: &[u8], constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> Result<BTreeMap<u32, Opcode>, ()> {
7171
let mut result = BTreeMap::new();
7272

7373
let mut data = code;
74-
loop {
74+
while !data.is_empty() {
7575
let offset = unsafe { data.as_ptr().offset_from(code.as_ptr()) } as usize;
76-
if let Ok((remaining, opcode)) = Opcode::parse(data, offset, constant_pool) {
77-
result.insert(offset as _, opcode);
78-
79-
data = remaining;
80-
} else {
81-
break;
76+
let (remaining, opcode) = Opcode::parse(data, offset, constant_pool).map_err(|_| ())?;
77+
if remaining.len() >= data.len() {
78+
return Err(());
8279
}
80+
result.insert(offset as _, opcode);
81+
data = remaining;
8382
}
8483

85-
result
84+
Ok(result)
8685
}
8786
}
8887

@@ -114,8 +113,8 @@ impl LocalVariableTableEntry {
114113
(
115114
be_u16,
116115
be_u16,
117-
map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()),
118-
map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()),
116+
map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())),
117+
map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())),
119118
be_u16,
120119
),
121120
|(start_pc, length, name, descriptor, index)| Self {
@@ -152,7 +151,10 @@ pub enum AttributeInfo {
152151
impl AttributeInfo {
153152
pub fn parse<'a>(data: &'a [u8], constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> IResult<&'a [u8], Self> {
154153
map_res(
155-
(map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), flat_map(be_u32, take)),
154+
(
155+
map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())),
156+
flat_map(be_u32, take),
157+
),
156158
|(name, info): (_, &[u8])| {
157159
Ok::<_, nom::Err<_>>(match name.as_str() {
158160
"ConstantValue" => AttributeInfo::ConstantValue(Self::parse_constant_value(info, constant_pool)?.1),
@@ -180,11 +182,11 @@ impl AttributeInfo {
180182
}
181183

182184
fn parse_source_file<'a>(data: &'a [u8], constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> IResult<&'a [u8], Arc<String>> {
183-
map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()).parse(data)
185+
map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())).parse(data)
184186
}
185187

186188
fn parse_constant_value<'a>(data: &'a [u8], constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> IResult<&'a [u8], ConstantPoolReference> {
187-
map(be_u16, |x| ConstantPoolReference::from_constant_pool(constant_pool, x as _)).parse(data)
189+
map_res(be_u16, |x| ConstantPoolReference::from_constant_pool(constant_pool, x).ok_or(())).parse(data)
188190
}
189191

190192
fn parse_local_variable_table<'a>(

classfile/src/class.rs

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,46 @@ use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec};
22

33
use nom::{
44
IResult, Parser,
5+
error::{Error, ErrorKind},
56
multi::length_count,
67
number::complete::{be_u16, be_u32},
78
};
89

910
use java_constants::ClassAccessFlags;
1011

11-
use crate::{attribute::AttributeInfo, constant_pool::ConstantPoolItem, field::FieldInfo, interface::parse_interface, method::MethodInfo};
12+
use crate::{
13+
ClassFileError, attribute::AttributeInfo, constant_pool::ConstantPoolItem, field::FieldInfo, interface::parse_interface, method::MethodInfo,
14+
validation::validate_class,
15+
};
1216

1317
fn parse_this_class<'a>(data: &'a [u8], constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> IResult<&'a [u8], Arc<String>> {
1418
let (data, this_class) = be_u16(data)?;
15-
let class_name_index = constant_pool.get(&this_class).unwrap().class_name_index();
19+
let class_name_index = constant_pool
20+
.get(&this_class)
21+
.and_then(ConstantPoolItem::class_name_index)
22+
.ok_or_else(|| nom::Err::Error(Error::new(data, ErrorKind::Verify)))?;
23+
let class_name = constant_pool
24+
.get(&class_name_index)
25+
.and_then(ConstantPoolItem::utf8)
26+
.ok_or_else(|| nom::Err::Error(Error::new(data, ErrorKind::Verify)))?;
1627

17-
Ok((data, constant_pool.get(&class_name_index).unwrap().utf8()))
28+
Ok((data, class_name))
1829
}
1930

2031
fn parse_super_class<'a>(data: &'a [u8], constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> IResult<&'a [u8], Option<Arc<String>>> {
2132
let (data, super_class) = be_u16(data)?;
2233

2334
let super_class = if super_class != 0 {
24-
let class_name_index = constant_pool.get(&super_class).unwrap().class_name_index();
25-
Some(constant_pool.get(&class_name_index).unwrap().utf8())
35+
let class_name_index = constant_pool
36+
.get(&super_class)
37+
.and_then(ConstantPoolItem::class_name_index)
38+
.ok_or_else(|| nom::Err::Error(Error::new(data, ErrorKind::Verify)))?;
39+
Some(
40+
constant_pool
41+
.get(&class_name_index)
42+
.and_then(ConstantPoolItem::utf8)
43+
.ok_or_else(|| nom::Err::Error(Error::new(data, ErrorKind::Verify)))?,
44+
)
2645
} else {
2746
None
2847
};
@@ -80,12 +99,23 @@ impl ClassInfo {
8099
))
81100
}
82101

83-
pub fn parse(file: &[u8]) -> Option<Self> {
84-
let (remaining, result) = Self::parse_info(file).ok()?;
102+
pub fn parse(file: &[u8]) -> Result<Self, ClassFileError> {
103+
let (remaining, result) = Self::parse_info(file).map_err(|_| ClassFileError::InvalidFormat)?;
85104
if !remaining.is_empty() {
86-
return None;
105+
return Err(ClassFileError::InvalidFormat);
106+
}
107+
if result.major_version < 45 {
108+
return Err(ClassFileError::InvalidFormat);
109+
}
110+
if result.major_version > 70 {
111+
return Err(ClassFileError::UnsupportedVersion(result.major_version));
87112
}
113+
validate_class(&result)?;
114+
115+
Ok(result)
116+
}
88117

89-
Some(result)
118+
pub fn validate(&self) -> Result<(), ClassFileError> {
119+
validate_class(self)
90120
}
91121
}

0 commit comments

Comments
 (0)