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

Conversation

LucasSte
Copy link

@LucasSte LucasSte commented Oct 3, 2024

No description provided.

@LucasSte LucasSte force-pushed the stricter-verifier branch 2 times, most recently from d601cb6 to 90a02dc Compare October 3, 2024 20:21
@LucasSte LucasSte marked this pull request as ready for review October 3, 2024 20:25
@LucasSte LucasSte changed the title SBPF stricter verification constraints SIMD-0179: SBPF stricter verification constraints Oct 3, 2024
Copy link

@topointon-jump topointon-jump left a comment

Choose a reason for hiding this comment

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

Some of these stricter rules, especially those around restricting jump destinations at runtime, have performance implications. Are these stricter checks absolutely necessary? If so, I think there are other ways of achieving this security without compromising performance.

## 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

Comment on lines 58 to 62
`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.

Comment on lines +66 to +70
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.
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?

Copy link

@topointon-jump topointon-jump left a comment

Choose a reason for hiding this comment

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

Can we specify exactly how these rules are enforced:

  • Whether the check is performed at runtime or verification time
  • The exact logic (in pesudo-code)
  • Whether these can be checked in a single pass

Several of these seem like they would have performance implications, and precisely specifying them would help to determine if this is the case or not.

Comment on lines +53 to +56
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.

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.

@Lichtso
Copy link
Contributor

Lichtso commented Nov 3, 2024

  • Almost all of the checks are verification time, only exception is the callx target location check, that is at runtime
  • All our checks are O(1), O(log n) or O(n) and can be performed in a single forward scan of the bytecode
  • About the callx target location runtime check:
    • Our suggestion: We register functions in the dynamic symbol table, then call target verification can be done in the single scan together with all other instructions and callx target runtime check is between O(1) and O(log n)
    • Your suggestion: We register functions in the bytecode, then call target verification requires a second scan of the bytecode, and the callx target runtime check becomes O(1)

@Lichtso Lichtso closed this Nov 4, 2024
@Lichtso Lichtso reopened this Nov 4, 2024
@topointon-jump
Copy link

topointon-jump commented Nov 5, 2024

Almost all of the checks are verification time, only exception is the callx target location check, that is at runtime
All our checks are O(1), O(log n) or O(n) and can be performed in a single forward scan of the bytecode

Could we add more details about these checks and their performance properties to the SIMD itself, in pseudo-code? I was reading the SIMD without looking at the code, and a lot of these performance properties weren't clear until I read the code.

After reading the code most of my concerns turned out to have been unfounded - it would be great to move towards a world where SIMDs can be read independently of the code.

@topointon-jump
Copy link

topointon-jump commented Nov 5, 2024

Your suggestion: We register functions in the bytecode, then call target verification requires a second scan of the bytecode, and the callx target runtime check becomes O(1)

Just to be clear, our suggestion was

  • Change the bytecode emitted to have a special marker bytecode, signifying the start of the function
  • We can then just check for that bytecode marker in the next instruction right before we jump

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.

3 participants