← Aptos Intelligence Deep Dive

fix function checker for struct visibility (#19351)

By rahxephon89 Apr 8, 2026 Security Importance 8/10 Source

What specific code changed

Why this change was made

The compiler previously only considered the discriminator of a match expression when detecting struct unpacking. Nested patterns such as Outer::V { Inner { val } } were missed, allowing illegal packs/unpacks to slip through the visibility checker. Additionally, the visibility logic was hard‑coded to a language‑version gate, which did not distinguish between public, friend, package, or private structs. The new logic centralizes visibility checks via check_cross_module_struct_access, enabling precise enforcement of the Visibility enum.

How it works technically

In function_checker.rs the original branch was:

ExpData::Match(_, discriminator, _) => {
    let id = discriminator.node_id();
    if let Type::Struct(mid, sid, _) = env.get_node_type(id).drop_reference() {
        push(mid.qualified(sid), StructOpKind::Unpack, id);
    }
}

It has been replaced with a richer implementation:

// Match expression unpacks the discriminator; arm patterns may unpack nested structs
ExpData::Match(_, discriminator, arms) => {
    let disc_node = discriminator.node_id();
    // The discriminator itself is unpacked when it is a struct/enum type
    if let Type::Struct(mid, sid, _) = env.get_node_type(disc_node).drop_reference() {
        push(mid.qualified(sid), StructOpKind::Unpack, disc_node);
    }
    // Traverse sub‑patterns of each arm to catch nested struct unpacks not covered by
    // the discriminator check above (e.g., `Inner { val }` inside `Outer::V { Inner { val } }`).
    for arm in arms {
        let sub_pats: Vec<&Pattern> = match &arm.pattern {
            // Handle nested struct unpacks in sub‑patterns.
            Pattern::Struct(_, _, _, fields) => fields.iter().collect(),
            Pattern::Tuple(_, pats) => pats.iter().collect(),
            // No sub‑pattern
            _ => vec![],
        };
        for pat in sub_pats {
            if let Pattern::Struct(_, mid, sid, _) = pat {
                push(mid.qualified(*sid), StructOpKind::Unpack, pat.node_id());
            }
        }
    }
}

This code now walks each arm's pattern, extracts any Pattern::Struct or Pattern::Tuple sub‑patterns, and records an StructOpKind::Unpack for each nested struct.

In function_generator.rs the old visibility gate was a direct language‑version check:

if !struct_env
    .env()
    .language_version()
    .language_version_for_public_struct()
{
    fun_ctx.internal_error(...);
}

It now calls a dedicated helper:

if !fun_ctx.check_cross_module_struct_access(ctx, struct_env, fun_mid, "pack") {
    return;
}

The helper (defined elsewhere) inspects struct_env.visibility() (using the newly imported Visibility) and the caller module's relationship (friend, package, or private) to decide if the pack/unpack is allowed. The same change is applied to the unpack path.

Where it fits in the Aptos pipeline

The modifications live in the Move compiler's function checking and bytecode generation stages, which occur after the source parsing stage and before the Move VM execution stage. In the broader Aptos pipeline this is part of the execution → compilation flow: source Move → function_checker (type/visibility validation) → function_generator (bytecode emission) → VM execution → state storage.

Implications


ELI5 — Explain Like I'm 5

Rahxephon added a smarter inspector to the Move compiler. Imagine the compiler as a security guard who checks whether a piece of code is allowed to open a locked box (a struct). Before, the guard only looked at the front of the box and missed hidden compartments inside nested patterns, so some illegal openings slipped through.

Now the guard also opens every drawer inside the box (the match arms) and makes sure each nested struct follows the visibility rules—whether it’s private, friend‑only, or package‑only. The guard also got a new rulebook (Visibility) instead of just checking the language version, so it can decide correctly based on who owns the box.

The change matters because it stops bad contracts from cheating by packing or unpacking structs they shouldn’t touch, especially when those structs are hidden inside complex pattern matches or inline functions. The new tests show the compiler rejecting such attempts with clear error messages.


Other Deep Dives


View this report interactively with Advanced / ELI5 tabs at https://aptos-intelligence.vercel.app/#0e8dad7. Plain-text version: /reports/0e8dad7.txt.