Skip to content

feat(lang-v2): support Sysvar<SysvarInstructions> for introspection - #4944

Open
franRappazzini wants to merge 4 commits into
otter-sec:anchor-nextfrom
franRappazzini:anchor-next
Open

feat(lang-v2): support Sysvar<SysvarInstructions> for introspection#4944
franRappazzini wants to merge 4 commits into
otter-sec:anchor-nextfrom
franRappazzini:anchor-next

Conversation

@franRappazzini

@franRappazzini franRappazzini commented Aug 18, 2026

Copy link
Copy Markdown

Summary

Sysvar<T> was bound to T: PinocchioSysvar + SysvarId + Copy, which
only admits sysvars the runtime exposes through sol_get_sysvar. The
instructions sysvar is not one of them, so instruction introspection —
reading the other instructions in the current transaction — was
unreachable from a #[derive(Accounts)] struct.

This PR makes Sysvar<SysvarInstructions> a first-class account type:

#[derive(Accounts)]
pub struct Introspect {
    pub sysvar_instructions: Sysvar<SysvarInstructions>,
}

// Deref reaches pinocchio's introspection API.
let previous = ctx.accounts.sysvar_instructions.get_instruction_relative(-1)?;
let caller = previous.get_program_id();

Design

Splitting SysvarId from SysvarLoad

The two sysvar families are read in fundamentally different ways, so the
single trait that drove the wrapper is split in two:

Trait Answers Applies to
SysvarId (existing) what address must this account have? every sysvar
SysvarLoad (new) how do I obtain the value? every sysvar

Clock / Rent read through the syscall and never touch account data.
Instructions has no syscall at all — its value must be read out of the
account's data buffer, which means the account has to be passed in the
transaction and the wrapper has to keep a borrow of it.

Sysvar<T>'s bound relaxes to just SysvarLoad (which requires
SysvarId), and the now-redundant PhantomData<T> is dropped since the
value is already stored by value.

Why syscall sysvars get a macro instead of a blanket impl

A blanket impl<T: PinocchioSysvar> SysvarLoad for T would overlap the
SysvarInstructions impl, and rustc cannot rule the overlap out: proving
SysvarInstructions: !PinocchioSysvar is negative reasoning about a foreign
trait on a foreign type. Each syscall-backed sysvar therefore gets an
explicit impl, generated by a small impl_syscall_sysvar! macro that
pairs the address, the IDL string, and the get() call in one place.

The Instructions alias and its 'static borrow

pinocchio's Instructions<T> is generic over its data container. The
new alias pins it at the one T that can outlive load:

pub type SysvarInstructions =
    pinocchio::sysvars::instructions::Instructions<Ref<'static, [u8]>>;

SysvarLoad::read borrows the account data and transmutes the guard to
'static. This is the same pattern SerializedAccount::load already
uses: Ref stores raw pointers into runtime memory rather than into the
AccountView, so moving the view into the wrapper afterwards does not
invalidate the guard. Holding it for the wrapper's lifetime is what
blocks later mutable borrows of the same account.

Sysvar::load has already compared the address against INSTRUCTIONS_ID
by the time read runs, so it goes through new_unchecked rather than
pinocchio's TryFrom<&AccountView> — that avoids redoing the compare and
lets the Ref be transmuted alone instead of the whole Instructions<_>.

Under the guardrails feature, read also rejects a buffer too small to
hold the [u16 num][u16 current_index] skeleton, so a hand-rolled mock
view cannot underflow the pointer arithmetic in load_current_index.
The address check makes this unreachable for the genuine sysvar.

No proc-macro change

v2 resolves account types through trait dispatch rather than a name
table, so #[derive(Accounts)] picks the wrapper up with no codegen
change. The SysvarId impl on the generic Instructions<T> is kept so
the IDL address is available for any instantiation; only the alias — the
instantiation that can outlive load — gets SysvarLoad.

Tests

Wrapper unit tests (lang-v2/tests/account_wrapper_checks.rs) — five
cases driving a synthetic sysvar blob built to the runtime's exact
layout, so the pointer arithmetic in pinocchio's accessors is exercised
for real rather than mocked:

  • sysvar_instructions_reads_synthetic_blob
  • sysvar_instructions_load_rejects_wrong_address
  • sysvar_instructions_rejects_undersized_data
  • sysvar_instructions_rejects_out_of_range_index
  • sysvar_instructions_holds_a_shared_borrow_not_an_exclusive_one

Miri witness (lang-v2/tests/miri_wrapper_accounts.rs) — runs under
Tree Borrows in CI. Pins the claim behind the unsafe: the guard's
provenance survives the view being moved into the wrapper, and the borrow
flag is released exactly once on drop.

Integration on real SBF (tests-v2/) — a read_instructions handler
that introspects its own invocation through LiteSVM, asserting from
inside the program that relative index 0 carries the right program id,
this handler's discriminant, and the sysvar as its only readonly account:

  • read_instructions_introspects_the_current_instruction
  • read_instructions_rejects_wrong_sysvar — passing Rent instead trips
    the address compare before any data is borrowed

Diagnostics — the SlotHashes compile-fail case now asserts on
SysvarLoad, the bound an unsupported sysvar actually trips, and the
on_unimplemented note lists what is supported.

IDLsysvar_wrappers_surface_their_idl_address covers the full
chain the IDL builder reads (SysvarId::IDL_ADDRESS
IdlAccountType::__IDL_ADDRESS) for all three sysvars.

Also adds the wrapper to the account-types table in lang-v2/README.md.

Branch Target

anchor-next — this is v2-only work; the files it touches do not exist
on master. It is additive, not breaking: Sysvar<Clock> /
Sysvar<Rent> keep working unchanged, and the relaxed bound only widens
what Sysvar<T> accepts.

Out of scope

While adding the IDL test I noticed a pre-existing gap unrelated to this
change: impl IdlAccountType for Box<T> (lang-v2/src/accounts/boxed.rs)
propagates __IDL_ACCOUNT_ENTRY and __IDL_TYPE_DEF but not
__IDL_ADDRESS or __IDL_IS_SIGNER, so Box<Sysvar<Clock>>,
Box<Program<System>> and Box<Signer> lose that metadata in the IDL.
Left untouched here; happy to file an issue or fix it separately.

`Sysvar<T>` only wrapped syscall-backed sysvars, so instruction
introspection was unreachable from `#[derive(Accounts)]`.

Split the wrapper's bound in two: `SysvarId` still supplies the
well-known address, and a new `SysvarLoad` says how to read the value.
`Clock` / `Rent` read from `sol_get_sysvar`; `Instructions` has no
syscall and borrows the account data instead, holding a `'static` `Ref`
guard for the wrapper's lifetime as `SerializedAccount::load` does.

No proc-macro change needed — v2 dispatches account types by trait.

Covered by wrapper unit tests over a synthetic sysvar blob, a Miri
witness for the transmute, and LiteSVM tests on real SBF
@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown

@franRappazzini is attempting to deploy a commit to the OtterSec Team on Vercel.

A member of the Team first needs to authorize it.

@swaroop-osec swaroop-osec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lgtm!
Only concern is about Instructions import in Prelude which can cause name collisions. Since it's a generic name programs commonly use for their own types

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 55.88235% with 15 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (anchor-next@3e38a12). Learn more about missing BASE report.

Files with missing lines Patch % Lines
tests-v2/programs/accounts/src/lib.rs 43.47% 13 Missing ⚠️
lang-v2/src/accounts/sysvar.rs 81.81% 2 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@              Coverage Diff               @@
##             anchor-next    #4944   +/-   ##
==============================================
  Coverage               ?   47.90%           
==============================================
  Files                  ?      158           
  Lines                  ?    31180           
  Branches               ?        0           
==============================================
  Hits                   ?    14938           
  Misses                 ?    16242           
  Partials               ?        0           
Flag Coverage Δ
v2 47.90% <55.88%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread lang-v2/src/accounts/sysvar.rs Outdated
@0x4ka5h

0x4ka5h commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

#4944 looks like the right PR to take, but before we merge could you please rename the public alias to SysvarInstructions instead of exporting bare Instructions from the prelude? The bare name creates a real downstream prelude collision for crates that already define their own Instructions type and then write Sysvar<Instructions>. It would also be helpful to add a short doc comment explaining that the unusual name is intentional and exists specifically to avoid that collision. Please update the related examples/docs to use Sysvar<SysvarInstructions> as well.

… note

- Renamed the `Instructions` type alias to `SysvarInstructions` to prevent namespace collisions in the prelude for downstream programs.
- Removed the hardcoded list of supported sysvars from the `SysvarLoad` trait's diagnostic note to reduce maintenance overhead.
@franRappazzini franRappazzini changed the title feat(lang-v2): support Sysvar<Instructions> for introspection feat(lang-v2): support Sysvar<SysvarInstructions> for introspection Aug 21, 2026
@franRappazzini

Copy link
Copy Markdown
Author

@jamie-osec @swaroop-osec @0x4ka5h already checked and ready for merge! I updated the name for SysvarInstructions and removed the unnecessary notes on trait SysvarLoad

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.

5 participants