argotorg / argotorg/solidity

Pragma dash-suffix silently parses as wildcard range

Open
#16,810 0 comments 0 reactions 1 assignee Claimed by @cameel View on GitHub
bug :bug:
Dominant language
C++
Stars
25.7k
Forks
6.2k
Avg merge
2d 19h
Merged PRs (30d)
29

Description

## Description

`pragma solidity X.Y.Z-` is silently parsed as a SemVer RANGE expression (`>=X.Y.Z <= upper`) rather than as a pre-release version pin. The match-expression parser at `liblangutil/SemVerHandler.cpp:200-220` treats the `-` token as the SemVer range operator, and `parseMatchComponent` does NOT recognize pre-release suffixes (explicit TODO at `:253-256`).

Concretely, `pragma solidity 0.0.0-x;` parses as `>=0.0.0 <= x.x.x` — both bounds match every compiler version. A contract pinned to `0.0.0-x` compiles silently against solc 0.8.35-develop with NO mismatch warning. The same shape applies for `0.0.0-0`, `0.0.0-x.x`, `=0.0.0-x`, etc.

Observed behavior matrix against current `0.8.35-develop`:

| Pragma | Parses as | Matches 0.8.35-develop? | Diagnostic |
|--|--|--|--|
| `0.0.0-x` | `>=0.0.0 <= x.x.x` | YES | NONE |
| `0.0.0-0` | `>=0.0.0 <= 0` | YES | NONE |
| `0.0.0-x.x` | `>=0.0.0 <= x.x` | YES | NONE |
| `=0.0.0-x` | SAME (= prefix doesn't change `-` parsing) | YES | NONE |
| `0.0.0` | `=0.0.0` (no `-`) | NO | Error 5333 (mismatch) |
| `0.0.0-rc1` | parse fails — `rc1` not numeric | n/a | Error 1684 (invalid version) |
| `0.0.0-develop` | parse fails — `develop` not numeric | n/a | Error 1684 |
| `1.0.0-x` | `>=1.0.0 <= x.x.x` | NO (current < 1.0.0) | Error 5333 |
| `0.8.35-x` | `>=0.8.35 <= x.x.x` | NO (develop < 0.8.35) | Error 5333 |

Operational consequence:
1. A user writing `pragma solidity 0.0.0-rc1;` (intending to pin a pre-release) gets Error 1684 because `rc1` isn't numeric. But the same user trying `pragma solidity 0.0.0-1;` or `pragma solidity 0.0.0-x;` gets a SILENT range that matches ANY compiler — including future solc 0.9.x, 1.0.x.
2. Audit footgun: a `0.0.0-X` pragma in a contract source looks like a pre-release version pin to anyone familiar with SemVer; in reality it pins nothing.
3. Refactor footgun: a tool that updates pragmas from `^0.8.0` to `0.8.35-x` (intending "exactly 0.8.35 pre-release") would silently make the contract compile against ANY 0.8.35+ compiler.

Expected: either pre-release suffixes are parsed per SemVer spec in match expressions, or a warning is emitted when `-` is used in a pragma immediately after `X.Y.Z` with a suffix that parses as a single short token, alerting the user that they may have intended a pre-release pin but got a range.

Source-level evidence — `parseMatchExpression` at `liblangutil/SemVerHandler.cpp:200-220`:

```cpp
void SemVerMatchExpressionParser::parseMatchExpression()
{
// component - component (range)
// or component component* (conjunction)

SemVerMatchExpression::Conjunction range;
range.components.push_back(parseMatchComponent());
if (currentToken() == Token::Sub)
{
range.components[0].prefix = Token::GreaterThanOrEqual;
nextToken();
range.components.push_back(parseMatchComponent());
range.components[1].prefix = Token::LessThanOrEqual;
}
...
}
```

`parseMatchComponent` at `SemVerHandler.cpp:253-256` (the explicit TODO):

```cpp
// TODO we do not support pre and build version qualifiers for now in match expressions
// (but we do support them in the actual versions)
return component;
```

`parseVersionPart` at `SemVerHandler.cpp:262-265`:

```cpp
char c = currentChar();
nextChar();
if (c == 'x' || c == 'X' || c == '*')
return unsigned(-1);
```

So `x` in the post-dash position becomes `unsigned(-1)` (max), making the upper bound of the range effectively unbounded.

## Environment

- Compiler version: 0.8.35-develop.2026.5.5+commit.47b9dedd.Linux.g++
- Operating system: Linux Ubuntu Jammy

## Steps to Reproduce

Minimal source — a contract with a pragma that looks like a pre-release pin:

```solidity
pragma solidity 0.0.0-x; // parses as: >=0.0.0 <= x.x.x

contract C {
function value() external pure returns (uint) { return 0xCAFE; }
}
```

Compiling this against solc 0.8.35-develop succeeds silently — no Error 5333 mismatch, no warning. If the pragma had been correctly interpreted as "pin to 0.0.0-x" (which no released compiler matches), compilation should have failed.

Forge test verifying the silent acceptance:

```
$ forge test --match-contract '^E43878d_' -vv
[PASS] test_compiles_against_any_version() (gas: 5735)
```

The same silent-acceptance occurs for `0.0.0-0`, `0.0.0-x.x`, and `=0.0.0-x`. Replacing the suffix with a non-numeric token (e.g. `0.0.0-rc1`) instead produces Error 1684, and dropping the dash entirely (`0.0.0`) produces Error 5333 — confirming the silent path is unique to numeric/wildcard suffixes.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.