Skip to content

interpreter: Fix out-of-range execution ProgramCounter #303

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

aman4150
Copy link
Collaborator

@aman4150 aman4150 commented Jun 2, 2025

Currently, the program counter always returns 0 when the last instruction is branch instruction and it evaluates to false. The correct behavior should be returning the faulting address.

This is also weird because the program counter returns correct value if the last instruction is something else.

Also, on re-compiler we do return correct faulting address.

This patch fixes interpreter behavior and makes it consistent with re-compiler.

@aman4150 aman4150 mentioned this pull request Jun 2, 2025
instance.set_reg(Reg::RA, crate::RETURN_TO_HOST);
instance.set_next_program_counter(ProgramCounter(0));
match_interrupt!(instance.run().unwrap(), InterruptKind::Trap);
assert_eq!(instance.program_counter(), Some(ProgramCounter(9)));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't hardcode this offset. Parse the instructions to get the offset (e.g. scroll up and take a look at the previous simple_test where it does something like that).

@@ -3797,6 +3814,8 @@ run_tests! {
trapping_preserves_all_registers_normal_trap
trapping_preserves_all_registers_segfault

test_out_of_range_execution
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_out_of_range_execution -> out_of_range_execution

Comment on lines 1046 to 1061
let (pc, branch_taken) = if callback(s1, s2) {
(ProgramCounter(target_true), true)
} else {
(ProgramCounter(target_false), false)
};

if let Some(target) = self.inner.resolve_jump::<DEBUG>(pc) {
Some(target)
} else {
Some(target_false)
self.inner.unresolved_program_counter = Some(pc);
if branch_taken {
Some(TARGET_INVALID_BRANCH)
} else {
Some(TARGET_OUT_OF_RANGE)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I don't think we want to do this here.

The whole point of the two-stage lazy "compilation" thing we have here in the interpreter is that we can do work like resolving jumps only once when we "compile" a given instruction, and then for any subsequent execution that work can be skipped. That's why by default the branches initially trigger the unresolved_* handlers, which resolve the jumps and replace the handler in-place with one that doesn't need to do it again.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the latest iteration.

@aman4150 aman4150 force-pushed the interpreter_oor_exec branch 2 times, most recently from e110acd to e2b67d2 Compare June 16, 2025 16:53
Currently, the program counter always returns 0 when the last instruction
is branch instruction and it evaluates to false. The correct behavior
should be returning the faulting address.

This is also weird because the program counter returns correct value if
the last instruction is something else.

Also, on re-compiler we do return correct faulting address.

This patch fixes interpreter behavior and makes it consistent with
re-compiler.

Signed-off-by: Aman <[email protected]>
@aman4150 aman4150 force-pushed the interpreter_oor_exec branch from e2b67d2 to 819d36c Compare June 16, 2025 21:27
Comment on lines +49 to +50
- name: Install clippy (zygote toolchain)
run: cd crates/polkavm-zygote && rustup component add clippy
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unnecessary.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, we already do this for ubuntu-* setups.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is clippy for zygote. Zygote is Linux-only, and this is action for macOS.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, okay.

Comment on lines 1044 to 1064
let s1 = self.get64(s1);
let s2 = self.get64(s2);
if callback(s1, s2) {
Some(target_true)

let (packed_target, branch_taken) = if callback(s1, s2) {
(target_true, true)
} else {
(target_false, false)
};

let (is_jump_target_valid, target) = InterpretedInstance::unpack_target(NonZeroU32::new(packed_target).unwrap());

if !is_jump_target_valid {
self.inner.unresolved_program_counter = Some(ProgramCounter(target));
if branch_taken {
Some(TARGET_INVALID_BRANCH)
} else {
Some(TARGET_OUT_OF_RANGE)
}
} else {
Some(target_false)
Some(target)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still not exactly what we want. :P

In general we'd want this handler to stay exactly the same way as it was, and instead have separate handler(s) for this special case. Again, we want to minimize the execution cost - the vast majority of branches will not need this, so they shouldn't pay the cost of handling this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extra penalty is testing the MSB now. Is that a lot?

I don't think there is any other way to implement this (without major refactoring, I think).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't need major refactoring; just use a separate handler for cases where the jump target is not valid, no?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for my understanding, you are saying we should go ahead with defining all 17 handlers for unresolved jump flavor?

I still don't follow how testing MSB is adding complexity though. As you already said, most of the code would not trigger this out-of-range logic, the branch predictor would quickly learn to execute non-out-of-range code path, and there is no penalty to be paid.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is no penalty to be paid

There's always a penalty, even if this won't trigger a branch miss, and branches are very common.

Just for my understanding, you are saying we should go ahead with defining all 17 handlers for unresolved jump flavor?

No, you don't need to explicitly define new handlers. Check which type of handler needs to be used in handle_unresolved_branch, then add a new const generic param to the handlers which will decide whether it's a branch with an invalid jump target (so then you don't have to do this check at runtime inside the handler).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants