Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,27 @@ jobs:
python-version: "3.13"
- name: Property fuzz (50k draws, rotating seed)
run: python tests/test_codegen_props.py 50000 ${{ github.run_number }}

# Prove the lowering is real: take the generated C# and put it through the
# actual .NET compiler (the PoC sandbox has no SDK, so this is the only place
# the golden example is genuinely compiled and run, not "verified by
# construction").
dotnet-golden:
name: golden C# compiles & runs (.NET)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- uses: actions/setup-dotnet@v4
Comment on lines +54 to +58

@coderabbitai coderabbitai Bot Jun 14, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pin GitHub Actions by commit SHA and disable checkout credential persistence.

Line 54 / Line 55 / Line 58 are unpinned action refs, and Line 54 should set persist-credentials: false to reduce token exposure risk.

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 54-54: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 54-54: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 55-55: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 58-58: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 54 - 58, The GitHub Actions in the
workflow are using version tags (v4, v5, v4) instead of being pinned to specific
commit SHAs, which is a security best practice. Additionally, the
actions/checkout action should include persist-credentials: false to reduce
token exposure risk. Replace the version tag references for actions/checkout@v4,
actions/setup-python@v5, and actions/setup-dotnet@v4 with their full commit SHA
pins, and add the persist-credentials: false configuration to the
actions/checkout step to minimize security vulnerabilities.

Source: Linters/SAST tools

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Skipping for now (deliberate). SHA-pinning every action is a repo-wide policy choice that's better maintained by Dependabot than hand-pinned in one PR, and these jobs only checkout + run tests — no push, no secrets — so the credential-persistence exposure is minimal. Happy to do a dedicated hardening pass (full SHA pins + persist-credentials: false) if you'd like that as a standing policy.


Generated by Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@PhysShell, that's a reasonable call — Dependabot's actions ecosystem update is exactly the right layer for maintaining SHA pins at scale, and the exposure here really is low without push permissions or secrets in scope.

Would you like me to open a follow-up GitHub issue to track the dedicated hardening pass (full SHA pins + persist-credentials: false across all workflow jobs)? That way it won't get lost once the PR merges.


✏️ Learnings added
Learnt from: PhysShell
Repo: PhysShell/Own.NET PR: 6
File: .github/workflows/ci.yml:54-58
Timestamp: 2026-06-14T16:16:25.503Z
Learning: In the PhysShell/Own.NET repository, SHA-pinning of GitHub Actions (e.g., actions/checkout, actions/setup-python, actions/setup-dotnet) is treated as a repo-wide policy decision to be handled by Dependabot rather than hand-pinned in individual PRs. Adding `persist-credentials: false` to checkout steps is also deferred to a dedicated hardening pass, not enforced per-PR. Do not flag unpinned action refs or missing persist-credentials as blocking issues in this repo.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

with:
dotnet-version: "8.0.x"
- name: Check the emitted method is still in sync with the golden host
run: python examples/golden_arraypool/verify_emit.py
- name: Compile & run the generated C# with the real compiler
run: |
dotnet new console -o "$RUNNER_TEMP/golden_app"
cp examples/golden_arraypool/Program.cs "$RUNNER_TEMP/golden_app/Program.cs"
dotnet run --project "$RUNNER_TEMP/golden_app"

50 changes: 47 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,42 @@ python tests/run_tests.py # кейсы + cod
`check` возвращает ненулевой код при наличии ошибок — годится для CI.
`emit` **отказывается** генерировать C#, если в `.own` есть хоть одна ошибка.

### Что оно ловит — галерея

В `examples/gallery/` лежат маленькие программы «как в жизни»: каждая роняет ровно
одну диагностику и снабжена C#-аналогом в комментарии. Каждый файл прибит к своему
коду тестом (`tests/test_gallery.py`), так что демо не разъезжается с тем, что
checker реально делает. Прогнать всё разом:

```bash
python tests/test_gallery.py
```

| Файл | Код | Реальный C#-аналог |
|------|-----|--------------------|
| `01_leak_on_error_path` | **OWN001** | забыл `Dispose()` на early-out ветке |
| `02_use_after_release` | **OWN002** | обращение к стриму после `Dispose()` |
| `03_double_release` | **OWN003** | `Dispose()` дважды |
| `04_use_after_move` | **OWN005** | использовал значение после передачи владения |
| `05_dispose_while_view_live` | **OWN008** | `ArrayPool.Return`, пока жив `Span<byte>` над массивом |
| `06_exclusive_while_shared` | **OWN006** | пишут через `Span`, который алиасит живой `ReadOnlySpan` |
| `07_use_after_handoff` | **OWN002** | тронул буфер после того, как его забрал вызов |
| `08_stack_buffer_escapes` | **OWN015** | вернул `Span<byte>` над `stackalloc` (dangling) |
| `09_untracked_call` | **OWN040** | владение «отмыли» через непрозрачный вызов |

`00_ok_clean` — чистый happy-path (rent → view → return), лоуэрится в exception-safe
`ArrayPool` Rent/Return.

`check` печатает ошибку в стиле rustc — `file:line:col`, сама строка исходника и
каретка под виновным именем:

```text
$ python -m ownlang check examples/gallery/05_dispose_while_view_live.own
examples/gallery/05_dispose_while_view_live.own:9:13: error: [OWN008] cannot release 'b' while it is borrowed
9 | release b; // freeing the backing store while `view` is alive
^
```

### Golden-пример: настоящий ArrayPool

```bash
Expand Down Expand Up @@ -536,9 +572,11 @@ fallback = pool`) — тоже **OWN030**: конфликтующее обеща
3. **Циклы и async отвергаются, а не анализируются** (OWN020). Нужен worklist с
fixpoint и loop-инварианты владения; CFG к этому готов (DAG-проход → worklist).

4. **C# в песочнице не запускается.** Компилятора нет, поэтому golden-пример
проверен *по построению* и чекером, **не исполнен**. У себя: `dotnet run`
в `examples/golden_arraypool`.
4. **В песочнице PoC нет .NET** — golden проверен *по построению* и чекером. Но
**CI его реально компилирует и запускает** настоящим компилятором (job
`dotnet-golden`: сверяет emit-вывод с host'ом, затем `dotnet run`), так что
лоуэрение проверено исполнением — просто не в этой песочнице. У себя: `dotnet
run` в `examples/golden_arraypool`.

5. **Нет настоящей системы типов.** Ресурсы номинальные, аргументы `acquire` не
типизируются, арифметики нет. Условие в `if` — непрозрачный текст: моделируется
Expand All @@ -547,6 +585,12 @@ fallback = pool`) — тоже **OWN030**: конфликтующее обеща

6. **Запрещено shadowing** (OWN031). Rust разрешает; для PoC запрет проще.

7. **CI-экшены не запинены по commit-SHA** (`actions/checkout@v4` и пр. на тегах,
без `persist-credentials: false`) — SAST (zizmor) это флагует. Сознательно
отложено: SHA-пиннинг — repo-wide политика, которую ведёт Dependabot / отдельный
hardening-проход, а не один PR; джобы только checkout + прогон тестов, без push
и без секретов, так что экспозиция минимальна.

---

## Как это ложится на твои документы
Expand Down
45 changes: 45 additions & 0 deletions corpus/real-world/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Real-world corpus

Не «Damn Vulnerable .NET app», а **Damn Leaky Resource Corpus**: настоящие
паттерны багов владения ресурсами из реального .NET-кода (ArrayPool, `Dispose`,
pooled buffers), сведённые к минимальной OwnLang-модели, на которой видно, как
checker их ловит.

Каждый кейс — папка с пятью файлами:

| Файл | Что это |
|------|---------|
| `before.cs` | багованный C#-фрагмент (паттерн как в реальном коде) |
| `after.cs` | исправленная версия |
| `case.own` | минимальная OwnLang-модель того же владения |
| `expected-diagnostics.txt` | коды, которые checker обязан выдать на `case.own` |
| `notes.md` | паттерн, источник и честная рамка |

Прогон (часть общего сьюта — `tests/test_corpus.py`):

```bash
python tests/test_corpus.py
```

## Честная рамка (читать обязательно)

`case.own` — это **ручная редукция** C#-паттерна, а **не** C#, который checker
прочитал: у OwnLang нет фронтенда по C#. Корпус доказывает, что **логика
владения ложится на настоящие баги** — будь это написано на OwnLang, checker бы
отверг. `before.cs`/`after.cs` репрезентативны для паттерна, это не дословный
дифф одного PR.

## Что выразимо, а что нет

Сейчас checker берёт «голый borrow»: leak / use-after-return / double-return /
escape / release-while-borrowed. **Не** выразимо (нужны новые модели — roadmap):
over-clear по границе `Span` (нет region/length-анализа), утечка только на
exception-path (анализ не моделирует исключения), concurrent `Dispose` (нет
конкурентности), async.

## Кейсы

| Кейс | Код | Реальный паттерн |
|------|-----|------------------|
| `arraypool-use-after-return` | OWN002 | rented-буфер вернули в пул, потом ещё читали slice |
| `arraypool-double-return` | OWN003 | один и тот же массив вернули в ArrayPool дважды ([#33767](https://github.com/dotnet/runtime/issues/33767)) |
15 changes: 15 additions & 0 deletions corpus/real-world/arraypool-double-return/after.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// AFTER (fixed): return exactly once, in finally.
using System.Buffers;

static void Use(int n)
{
int[] rented = ArrayPool<int>.Shared.Rent(n);
try
{
Work(rented);
}
finally
{
ArrayPool<int>.Shared.Return(rented);
}
}
19 changes: 19 additions & 0 deletions corpus/real-world/arraypool-double-return/before.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// BEFORE (buggy). Reduction of dotnet/runtime#33767 "Do not double-return
// arrays to ArrayPool": the same rented array is returned twice (here a Return
// on the success path AND a Return in finally). A double-return corrupts the
// pool — the array can later be rented out to two callers at once.
using System.Buffers;

static void Use(int n)
{
int[] rented = ArrayPool<int>.Shared.Rent(n);
try
{
Work(rented);
ArrayPool<int>.Shared.Return(rented); // returned here ...
}
finally
{
ArrayPool<int>.Shared.Return(rented); // <-- ... and again here (double)
}
}
14 changes: 14 additions & 0 deletions corpus/real-world/arraypool-double-return/case.own
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// OwnLang model: returning the same buffer twice is a double `release`.
module Corpus
resource Buffer {
acquire rent
release give
emit_type "byte[]"
emit_acquire "ArrayPool<byte>.Shared.Rent({args})"
emit_release "ArrayPool<byte>.Shared.Return({0})"
}
fn run(n: int) {
let rented = acquire Buffer(n); // ArrayPool.Rent
release rented; // ArrayPool.Return
release rented; // returned again -> OWN003
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OWN003
15 changes: 15 additions & 0 deletions corpus/real-world/arraypool-double-return/notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# ArrayPool double-return

**Pattern:** the same rented array is returned to `ArrayPool<T>.Shared` twice —
classically a `Return` on a normal path plus another in a `finally`/`Dispose`,
or two `Dispose()` calls. The pool can then hand the same array to two renters
simultaneously. See dotnet/runtime#33767 ("Do not double-return arrays to
ArrayPool").

**What the checker says:** the OwnLang model trips **OWN003** (double release).

**Honesty / scope.** As with the other cases, `case.own` is a faithful hand
reduction of the pattern (not C# ingested by the checker); `before.cs` /
`after.cs` are representative of the bug and its fix, not a verbatim PR diff.

Reference: https://github.com/dotnet/runtime/issues/33767
11 changes: 11 additions & 0 deletions corpus/real-world/arraypool-use-after-return/after.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// AFTER (fixed): consume the buffer BEFORE returning it to the pool.
using System.Buffers;

static int[] Divide(int dividend, int divisor)
{
int[] quotient = ArrayPool<int>.Shared.Rent(Size(dividend));
Compute(quotient, dividend, divisor);
int[] result = BuildResult(quotient); // consume first ...
ArrayPool<int>.Shared.Return(quotient); // ... then return
return result;
}
14 changes: 14 additions & 0 deletions corpus/real-world/arraypool-use-after-return/before.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// BEFORE (buggy). Reduction of the classic ArrayPool use-after-return bug seen
// across dotnet/runtime's buffer-pooling code (e.g. the BigInteger division
// path): a rented buffer is returned to the pool, then a slice of it is still
// read while building the result. Representative of the pattern, not verbatim
// from one PR.
using System.Buffers;

static int[] Divide(int dividend, int divisor)
{
int[] quotient = ArrayPool<int>.Shared.Rent(Size(dividend));
Compute(quotient, dividend, divisor);
ArrayPool<int>.Shared.Return(quotient); // <-- returned to the pool here ...
return BuildResult(quotient); // <-- ... but still read here (UAF)
}
18 changes: 18 additions & 0 deletions corpus/real-world/arraypool-use-after-return/case.own
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// OwnLang model. `acquire` == ArrayPool.Rent, `release` == ArrayPool.Return,
// a `borrow`/call == reading a Span over the buffer. Reading after `release`
// is the use-after-return the BEFORE code commits.
module Corpus
resource Buffer {
acquire rent
release give
emit_type "byte[]"
emit_acquire "ArrayPool<byte>.Shared.Rent({args})"
emit_release "ArrayPool<byte>.Shared.Return({0})"
emit_borrow "{0}.AsSpan()"
}
extern fn BuildResult(borrow Buffer);
fn divide(n: int) {
let quotient = acquire Buffer(n); // ArrayPool.Rent
release quotient; // ArrayPool.Return <-- too early
BuildResult(quotient); // read after return -> OWN002
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OWN002
27 changes: 27 additions & 0 deletions corpus/real-world/arraypool-use-after-return/notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# ArrayPool use-after-return

**Pattern:** a buffer rented from `ArrayPool<T>.Shared` is `Return`ed to the
pool, and then a slice of it is still read. Once returned, the array may be
handed to another renter, so the later read sees torn/foreign data. This is one
of the most common real ArrayPool bugs; it shows up repeatedly in
dotnet/runtime's buffer-pooling code (the BigInteger division/GCD path is the
oft-cited example).

**What the checker says:** the OwnLang model trips **OWN002** (use after
release) — `release` is `ArrayPool.Return`, and `BuildResult(quotient)` reads
the buffer afterwards.

```text
$ python -m ownlang check corpus/real-world/arraypool-use-after-return/case.own
case.own:14:14: error: [OWN002] borrow 'quotient' after it was released
14 | BuildResult(quotient); // read after return -> OWN002
^
```

**Honesty / scope.** `case.own` is a *hand reduction* of the C# pattern, not C#
the checker ingested — OwnLang has no C# front-end. It demonstrates that the
ownership *logic* maps onto the real bug: had the code been written in OwnLang,
the checker would have rejected it. The real-world specifics (the division math,
the exact slice bounds) are abstracted to `acquire`/`release`/`borrow`.
`before.cs` / `after.cs` capture the pattern; they are representative, not a
verbatim copy of a single PR.
19 changes: 19 additions & 0 deletions examples/gallery/00_ok_clean.own
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Clean: acquire, look at it mutably then read-only, hand it back.
// Lowers to ArrayPool Rent/Return inside a try/finally (exception-safe).
module Gallery
resource Buffer {
acquire rent
release give
emit_type "byte[]"
emit_acquire "ArrayPool<byte>.Shared.Rent({args})"
emit_release "ArrayPool<byte>.Shared.Return({0})"
emit_borrow "{0}.AsSpan()"
}
extern fn Fill(borrow_mut Buffer);
extern fn Hash(borrow Buffer);
fn process(size: int) {
let buf = acquire Buffer(size);
borrow_mut buf as bytes { Fill(bytes); }
borrow buf as view { Hash(view); }
release buf;
}
10 changes: 10 additions & 0 deletions examples/gallery/01_leak_on_error_path.own
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// OWN001 — leak on one path.
// Real C#: you Dispose() in the happy branch but forget it on the early-out.
module Gallery
resource Conn { acquire open release close }
fn handle(flag: int) {
let c = acquire Conn(flag);
if (flag) {
release c; // released here ...
} // ... but on the else path c is never closed -> leak
}
10 changes: 10 additions & 0 deletions examples/gallery/02_use_after_release.own
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// OWN002 — use after release (definite, on every path).
// Real C#: touching a stream after Dispose() -> ObjectDisposedException.
module Gallery
resource Conn { acquire open release close }
extern fn Send(borrow Conn);
fn run() {
let c = acquire Conn(1);
release c;
Send(c); // c is already closed
}
9 changes: 9 additions & 0 deletions examples/gallery/03_double_release.own
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// OWN003 — double release.
// Real C#: Dispose() called twice (and the type isn't idempotent).
module Gallery
resource Conn { acquire open release close }
fn run() {
let c = acquire Conn(1);
release c;
release c;
}
11 changes: 11 additions & 0 deletions examples/gallery/04_use_after_move.own
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// OWN005 — use after ownership was moved away.
// Real C#: using a buffer after you handed the only owner to someone else.
module Gallery
resource Buffer { acquire rent release give }
extern fn Hash(borrow Buffer);
fn run() {
let a = acquire Buffer(64);
let b = move a; // ownership transferred to b
Hash(a); // a is now empty
release b;
}
12 changes: 12 additions & 0 deletions examples/gallery/05_dispose_while_view_live.own
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// OWN008 — release while a borrow (a view) is still outstanding.
// Real C#: returning the array to ArrayPool while a Span<byte> over it is in use.
module Gallery
resource Buffer { acquire rent release give }
extern fn Fill(borrow_mut Buffer);
fn run() {
let b = acquire Buffer(64);
borrow_mut b as view {
release b; // freeing the backing store while `view` is alive
Fill(view);
}
}
12 changes: 12 additions & 0 deletions examples/gallery/06_exclusive_while_shared.own
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// OWN006 — exclusive (mutable) access while a shared view is live.
// Real C#: writing through one Span while another ReadOnlySpan aliases it.
module Gallery
resource Buffer { acquire rent release give }
extern fn Fill(borrow_mut Buffer);
fn run() {
let b = acquire Buffer(64);
borrow b as readers {
Fill(b); // needs exclusive access, but `readers` is live
}
release b;
}
11 changes: 11 additions & 0 deletions examples/gallery/07_use_after_handoff.own
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// OWN002 — use after ownership was consumed by a callee.
// Real C#: a method takes ownership (it will Dispose), then you touch it again.
module Gallery
resource Buffer { acquire rent release give }
extern fn Store(consume Buffer);
extern fn Hash(borrow Buffer);
fn run() {
let b = acquire Buffer(64);
Store(b); // Store now owns b (and will free it)
Hash(b); // ... but we used it afterwards
}
Loading
Loading