Skip to content

bug: embedding unrelated schema into abi of contract function #1510

@hlgltvnnk

Description

@hlgltvnnk

near-sdk macro #[near] doesnt propagate #[cfg(...)] attributes from individual methods into their abi entries inside the grouped near_abi_* symbol. The #[near] proc macro runs before #[cfg] is evaluated.
If the function is part of a contract extension and it is under the cfg/cfg_attr macro (or other), during abi schema generation it is merged with the previous function without the macro, which leads to such consequences:

  1. skipping schema generation for those functions - so we cant find it in dylib with _near_abi prefix (BUT it is merged with previous one)
  2. omitting any macro restrictions on which this function is under

Simple example:
So if we have such implementation:

#[near]
impl SomeContractExt for Contract {
    fn first(&self) {}

    #[cfg_attr(not(feature = "some_feat"), access_control_any(roles(Role::FirstRole)))]
    #[cfg_attr(feature = "some_feat", access_control_any(roles(Role::SecondRole)))]
    fn second(&self) {}

    #[cfg(feature = "some_feat")]
    fn third(&self) {}
}

After abi generation without "some_feat" feature we expect to have only first and second methods to be present in abi, but all 3 are present
Moreover there will be only __near_abi_first in dylib

Real world example
We have fn under "far" feature which should not be in abi during regular builds

How to reproduce:

  1. Generate abi (there will be force_add_public_keys and force_remove_public_keys):
    cargo near abi --manifest-path ./defuse/Cargo.toml --features=contract,abi --out-dir abi

  2. Check with symbol dumber (there will be only ___near_abi_is_account_locked - nothing more from this ext):
    nm -g target/debug/libdefuse.dylib

  3. Try to fetch chunked abi entry:

        let lib = libloading::Library::new(dylib_path).unwrap();

        let symbol = "__near_abi_is_account_locked";
        let entry: libloading::Symbol<extern "C" fn() -> (*const u8, usize)> =
            lib.get(symbol.as_bytes()).unwrap();

        let (ptr, len) = entry();
        let data = Vec::from_raw_parts(ptr as *mut _, len, len);
        let result: ChunkedAbiEntry = serde_json::from_slice(&data).unwrap();

The #[near] macro sees all methods, generates and bakes them into a single near abi. The #cfg(feature = "far")] attribute ends up in non bindgen attrs and gets passed through to the original method - but the abi entry is already hardcoded into the near abi is_account_locked function body with no cfg gate, regardless of whether the far feature is enabled
Wasm wrapper for fn under feature would be stripped by cfg, but the abi entry persists because its embedded inside a different functions body

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Status

NEW❗

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions