Skip to content

FinalizeTypes::matches_struct rejects valid cast to external struct with member-access operands #3330

Description

@mohammadfawaz

Summary

FinalizeTypes::matches_struct validates a cast to an external struct using a single Stack, and reuses that stack both to look up the struct definition and to resolve the operand register types. When it is called with the external program's stack (which is correct for the struct definition), it then resolves the cast's operand member accesses against that same external stack — so a member access on an external-struct register calls get_external_stack(<external program>) while already on that program's stack, and fails:

Attempted to get the main program '<external program>' as an external program.

The analogous RegisterTypes::matches_struct (functions/transitions) takes two stacks and does not have this problem — which is why the bug is finalize-only.

Version

v4.8.1 (commit b7f0859)

Reproducer (pure Aleo)

Add both programs to a Process (dependency order: lib.aleo first).

lib.aleo

program lib.aleo;

struct S:
    hi as u128;
    lo as u128;

function noop:

constructor:
    assert.eq edition 0u16;

consumer.aleo

import lib.aleo;
program consumer.aleo;

function run:
    input r0 as lib.aleo/S.private;
    async run r0 into r1;
    output r1 as consumer.aleo/run.future;

finalize run:
    input r0 as lib.aleo/S.public;
    cast r0.hi r0.lo into r1 as lib.aleo/S;   // <-- member-access operands into an external-struct cast
    assert.eq r1.lo r0.lo;

constructor:
    assert.eq edition 0u16;

process.add_program(&consumer) fails during finalize type-checking with the error above. The same construction in a transition body (function, not finalize) validates fine, e.g. cast r0.hi r1.lo into r2 as lib.aleo/S.

Root cause

The two struct-cast type-checkers diverge (paths in synthesizer/process/src/stack):

  • Transitionregister_types/matches.rs::matches_struct takes two stacks:

    pub fn matches_struct(&self, operands_stack: &Stack<N>, stack: &Stack<N>, operands, struct_) -> Result<()>

    Call site register_types/initialize.rs:550:

    self.matches_struct(stack, &*external_stack, instruction.operands(), struct_)?;

    Operand types are resolved against operands_stack (the caller), the struct definition against stack (external). Correct.

  • Finalizefinalize_types/matches.rs::matches_struct takes only one stack:

    pub fn matches_struct(&self, stack: &Stack<N>, operands, struct_) -> Result<()>

    Call site finalize_types/initialize.rs:999:

    self.matches_struct(&*external_stack, instruction.operands(), struct_)?;

    Inside, operand register types are resolved with self.get_type(stack, register) where stack is the external stack. For a member-access operand, get_type reaches the ExternalStruct(..) member branch (finalize_types/mod.rs:186):

    let external_stack = stack.get_external_stack(locator.program_id())?;

    Here stack already is that external program's stack, so program_id == self.program.id() and get_external_stack bails (helpers/stack_trait.rs:265).

So this is an inconsistency between the two parallel type-checkers: RegisterTypes::matches_struct was given a separate stack for operand-type resolution vs. struct-definition lookup, but FinalizeTypes::matches_struct was not.

Suggested fix

Give FinalizeTypes::matches_struct the same two-stack signature as RegisterTypes::matches_struct (an operands_stack for get_type on the operands, and the external stack for the struct definition), and update the finalize_types/initialize.rs:999 call site to pass both. Cast semantics for external structs in finalize should match transitions.

Context

Found via a Leo miscompile (ProvableHQ/leo#29586). Leo currently works around it in code generation by spilling member-access operands into plain registers before an external-struct cast in a finalize, but the underlying defect is here in snarkVM.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions