Skip to content
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

SIMD-0179: SBPF stricter verification constraints #179

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions proposals/0179-stricter-verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
simd: '0179'
title: SBPF Stricter verification constraints
authors:
- Alexander Meißner
- Lucas Steuernagel
category: Standard
type: Core
status: Draft
created: 2024-10-03
---

## Summary

This SIMD proposes improvements in the verification rules of SBPF programs.
The introduction of the syscall instruction in SIMD-0178 allows for the
differentiation of internal and external calls, opening up for stricter
verification constraints and increased security of deployed programs.

## Motivation

The `call` instruction (opcode `0x85`) in SBF, especially when it requires a
relocation, is ambiguous. It does not carry information about whether it is
a system call or an internal call. This ambiguity prevents more rigid
verification constraints that would reject programs jumping to inconvenient
locations, like invalid destinations or the middle of an `lddw` instruction.
The new syscall instruction introduced in SIMD-0178 permits the
differentiation of internal and external calls, so we can introduce new
verification rules to prevent any unwarranted behavior.

## New Terminology

None.

## Detailed Design

The following must go into effect if and only if a program indicates the SBF
version XX or higher in its ELF header e_flags field, according to the

Choose a reason for hiding this comment

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

Let's specify this version

specification of SIMD-0161.

### Restrict functions’ last instruction

Functions must only end with the `ja` (opcode `0x05`) or the exit (opcode
`0x9D` since SIMD-0178) instruction. Allowing calls to be the last instruction
of functions was inconvenient, because when the call returns, and there is no
other instruction to redirect the control flow, we will execute the very next
program counter, resulting in a fallthrough into another function’s code.
Offending this new validation condition must throw an
`VerifierError::InvalidFunction` error.

### Restrict jump instruction destination

All jump instructions, except for `call` (opcode `0x85`) and `callx` (opcode
`0x8D`), must now jump to a code location inside their own function. Jumping
to arbitrary locations hinders a precise program verification.
`VerifierError::JumpOutOfCode` must be thrown for offending this rule.
Comment on lines +67 to +70

Choose a reason for hiding this comment

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

How is this enforced? Is this enforced in the verifier or at runtime?

Copy link
Contributor

@Lichtso Lichtso Nov 3, 2024

Choose a reason for hiding this comment

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

This is a check at verification time (see error code) and is easy to enforce:
The verifier scans all instructions and keeps track of which function it is currently in, then checks jump targets against that range. The current function tracking can be done in constant time (per scanned instruction) by checking the next symbol in the dynamic symbol table and then advancing that pointer once a function boundary is crossed.

See: https://github.com/solana-labs/rbpf/blob/69a52ec6a341bb7374d387173b5e6dc56218fe0c/src/verifier.rs#L238

Choose a reason for hiding this comment

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

Could we add this description^ to the SIMD? 🙏

Copy link
Author

Choose a reason for hiding this comment

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

Added.


`call imm` (opcode `0x85`) must only be allowed to jump to a program counter
previously registered as the start of a function. Otherwise
`VerifierError::InvalidFunction` must be thrown. Functions must be registered
if they are present in the symbol table. The entrypoint to the program must
also define a valid function.
Copy link

@topointon-jump topointon-jump Nov 1, 2024

Choose a reason for hiding this comment

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

Will this require a double pass through the bytecode to pick up all valid call destinations?

Copy link
Contributor

Choose a reason for hiding this comment

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

The symbol table is not the bytecode. Call destinations in the bytecode are not registered as functions.

Copy link
Contributor

Choose a reason for hiding this comment

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

Better wording might be:

Functions are registered by presence in the symbol table.

Copy link
Author

Choose a reason for hiding this comment

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

Functions are registered by presence in the symbol table.

I was going to adopt your wording, but basically I already say this:

Functions must be registered if they are present in the symbol table.

Is this dubious or ambiguious?

Copy link
Contributor

Choose a reason for hiding this comment

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

Issue is that it is unclear if there are other ways for functions to be registered.

Choose a reason for hiding this comment

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

Yea - could we specify that we are treating the symbol table as the source of truth for what is considered a valid call dest? 🙏

Copy link
Author

Choose a reason for hiding this comment

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

I included more details on this.


### Runtime check for callx

The jump destination of `callx` (opcode `0x8D`) must be checked during
execution time to match the initial address of a registered function. If this
is not the case, a `EbpfError::UnsupportedInstruction` must be thrown. This
measure is supposed to improve security of programs, disallowing the malicious
use of callx.
Comment on lines +89 to +93
Copy link

@topointon-jump topointon-jump Nov 1, 2024

Choose a reason for hiding this comment

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

This has performance implications - is this absolutely necessary? If this check is necessary, can we instead have the compiler mark the beginning of valid jump destinations by emitting a marker bytecode opcode?


A function is registered according to the rules mentioned in the previous
section: be present in the symbol table or be the entrypoint.

### Limit where a function can start

Presently, functions may start in any part of the ELF text section, however
this is an encumbrance when it comes to `lddw` (opcode `0x18`) instructions.
As they occupy two instruction slots in an ELF, it is possible for functions
to start between these lddw slots, very likely resulting in undesired
behavior. The verifier must throw an `VerifierError::JumpToMiddleOfLddw` error
when that is the case.

### Removal of ExecutionOverrun error

As the jump instructions destinations are now limited to the range of the
function they are located in, and the call instructions can only move the
program counter to a known location, the `EbpfError::ExecutionOverrun` must not
be reachable, so it must be removed.

## Alternatives Considered

None.

## Impact

The changes proposed in this SIMD are transparent to dApp developers. The
compiler toolchain will emit correct code for the specified SBF version. The
enhanced verification restrictions will improve the security of programs
deployed to the blockchain, filtering out unwarranted behavior that could be
used with malicious intents.

## Security Considerations

None.
Loading