Skip to content

fix: only a definite release makes a call-site handoff (INF-S2, #380) - #381

Merged
PhysShell merged 3 commits into
mainfrom
ccr-d1573cc1-g41e75
Sep 29, 2026
Merged

PhysShell merged 3 commits into
mainfrom
ccr-d1573cc1-g41e75

Conversation

@PhysShell

@PhysShell PhysShell commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Что и зачем

ConsumesParam считал параметр поглощённым, если в теле вызываемого есть любой Dispose/Close этого параметра. Поэтому release под guard'ом превращался в безусловный release аргумента на стороне вызывающего — ровно тот сфабрикованный must, который запрещает INF-S2. На закреплённом коде dotnet/runtime это давало ложные OWN009 у двух вызывающих VerifyPersistedKey(..., disposeKey: false) и ложную сводку must у публичной обёртки Mono.Options GetArguments(TextReader) (#380).

Теперь прямой release или форвард в другого потребителя считается поглощением только если он определён — выполняется на каждом нормальном пути вызываемого: не под условием, не в цикле или catch, не после раннего return (кроме покрытого finally). Значение guard'а не читается, OwnIR/schema/lattice/core не меняются. Отказ от handoff предотвращает фабрикацию безусловного must, но не даёт INF-S2 may: для отслеживаемых локалов это обычно деградирует в escape/молчание; для форвардимых параметров существующий вывод может получить no из оставшегося use — это намеренно вне #380 и не полное представление may. Представить частичный форвард как may можно только через каноничный call-факт.

This fix restores the precision floor by refusing to fabricate unconditional consume. It does not attempt to infer the correct may summary for guarded forwarding; that requires a canonical call representation and remains outside this change.

Конкретно: обёртка, форвардящая свой параметр в guarded-потребителя, теперь получает no, а не may, которое дал бы INF-S3. Для false (C16) это верно, для true/форварда — нет, но и не фабрикует ownership transfer. Это известное ограничение, а не цель, и оно запинено: GuardedConsumeSample.ForwardDynamic(Stream forwarded, bool dispose) → сводка no (KNOWN LIMITATION). Пин громко падает при любом сдвиге: must = сфабрикованный consume вернулся; may = пришли каноничные call-факты, перезаписать пин и перепроверить условие 1 #304.

Production-код: frontend/roslyn/OwnSharp.Extractor/Program.cs — логика только в f164dd3; 410431a меняет в нём лишь комментарий IsDefiniteInBody (убрано завышенное «обратный вердикт не изобретается», область гарантии сужена до «нет сфабрикованного must»). Остальное — фикстура, тест, CI-шаг и переход записей P-037-evidence.

Переход записей P-037 (c662032, без изменения production-логики)

  • Контроли corpus/p036-bakeoff: current пиннил сфабрикованный release и вызванный им KNOWN_FALSE_POSITIVE OWN003. Перемерено на обоих движках; новое current совпадает с post_a1, который лежит в этих файлах с afeca38. Старая запись сохранена в append-only remeasured[].
  • Fact-shape census corpus/p037-shapes: 9 форм перезаписаны штатным record. У 7 guarded-вызывающих пропадает запись functions[] вместе с A2-сайдкаром; 2 обёртки release → use; у двух форм уходит OWN003. В каждой форме — baseline_transitions[] с дословно сохранёнными старыми фактами, вердиктами и a2_expect. Ожидания для исчезнувших записей заменены утверждённым отсутствием {"record": "absent"}, которое чекер теперь проверяет по имени: если запись вернётся, census упадёт, а не будет молча перезаписан.
  • Классификация: ACCEPTED BASELINE MOVEMENT CAUSED BY ConsumesParam flattens guarded release into unconditional handoff, causing false OWN009 and false must summaries #380 — не реализация P-037, не reopen Post-cutover: summary-backed lifecycle release reachability (generalize the landed #293/#302 predicates) #304 (условие 1 не выполнено: тело по-прежнему сворачивает first-party форварды, call-оп до движков не доходит), не доказательство выполнения A2. Подробно: docs/notes/p037-fact-shape-baseline-transition-380.md.

Тип изменения

  • feat — новая возможность
  • fix — исправление бага
  • docs — документация
  • refactor / chore / test / ci — без изменения поведения

Как проверено

  • python tests/run_tests.py — с OWN_TIERB_REQUIRED=1, OWEN_STAGE2_REQUIRE=1, OWEN_RUST_CORE: exit 0 (с .NET 8 + .NET 9 SDK; verify-target Tier B 65/65)
  • ruff check . и mypy (strict, включая изменённые scripts/p037_*.py)
  • селфтесты затронутых скриптов (python scripts/<...>.py --selftest)

Дополнительно:

  • tests/test_guarded_consume.py (новый, обязательный в CI-джобе wpf-extractor): 0 падений на фиксе, 8 падений на экстракторе 23e3203 (red → green); якоря безусловного handoff (обычный Dispose и Dispose в finally) по-прежнему дают OWN002. С 410431a там же пин KNOWN LIMITATION ForwardDynamic.forwarded == ["no"] (обе машины согласны).
  • scripts/p037_verdict_snapshot.py (137 файлов, population 23e3203, обе машины): сдвинулись 3 файла — все три снятие известного ложного OWN003 в gv4-контролях; новых находок нет.
  • scripts/p037_mos_snapshot.py: собственное дерево (81 файл) UNCHANGED; корпус — 12 сдвигов фактов, все в corpus/p036-bakeoff, межмашинный паритет 0 расхождений.
  • Факты экстрактора до/после побайтово равны для examples/gallery/cs (включая якорь 07_use_after_handoff), examples/flagship, fixtures/marketplace-consumer-demo и всех 103 существующих sample-функций.
  • scripts/p037_controls.py --engine both: all match (и --post-a1: all match); scripts/p037_fact_shapes.py check --engine both: 14/14.
  • Точные upstream-сайты: ложные OWN009 у вызывающих C12/C13 исчезли, ложный must за C16 стал no (evidence: PhysShell/Own.NET-paperwork, paper-eval/oss-sixcase-fix380-reobservation-v1.json).

Связанные issue

Closes #380. Refs #304 — не переоткрывается; reopen condition 1 не выполнено.

Чеклист

  • изменение покрыто тестом/селфтестом (или объяснено, почему нет)
  • README/docs обновлены при необходимости
  • коммиты в conventional-commit стиле (feat:, fix:, docs: …) — заголовки f164dd3 и c662032 не в этом стиле (410431a — в нём); f164dd3 уже цитируется внешними уликами (ConsumesParam flattens guarded release into unconditional handoff, causing false OWN009 and false must summaries #380, paperwork), поэтому история не переписывалась. Заголовок PR — в conventional-стиле.

🤖 Generated with Claude Code

https://claude.ai/code/session_017znWXJbuLfmfZ7Un9GXzNp

ConsumesParam treated a first-party parameter as consumed when the
callee contained any immediate Dispose/Close of it, so a release
behind a guard was lowered as an unconditional release of the
caller's argument. That is the flattened must INF-S2 forbids: on
pinned dotnet/runtime code it produced false OWN009 at two callers of
VerifyPersistedKey(..., disposeKey: false) and a false must summary
for the public Mono.Options GetArguments(TextReader) wrapper.

A direct release or a forward to another consumer now counts only
when it runs on every normal-return path of the callee: not under a
condition, loop or catch, and not behind an earlier return unless a
finally covers it. The guard's value is never read and no OwnIR field
changes. A declined argument stays an ordinary escape (an untracked
local, a used parameter), so no opposite verdict is invented.

GuardedConsumeSample + tests/test_guarded_consume.py pin the partial
shapes (guard false, guarded early return, guarded forward), the
guard-true caller (no fabricated release, no fabricated leak), the
definite anchors (plain and finally dispose still give OWN002 on a
later use) and the borrowing wrapper (summary no).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017znWXJbuLfmfZ7Un9GXzNp
…hapes

No production logic changes. #380 stopped lowering a guarded release
as a call-site release, which moves two records the P-037 evidence
pins; both are moved on purpose and in view.

Conformance controls: `current` pinned the fabricated release and, for
the three G-V4 controls, the known-false-positive OWN003 it caused. It
is re-measured on both engines and now equals the `post_a1` record
those files have carried since afeca38. The superseded record is kept
in each control's append-only remeasured[] entry.

Fact shapes: nine shapes re-recorded with `p037_fact_shapes.py record`.
Seven guarded callers lose their functions[] record and with it the A2
sidecar; two wrappers move release -> use; two verdicts lose OWN003.
Each moved shape gains a baseline_transitions[] entry keeping the
superseded facts, verdicts and a2_expect verbatim. The vanished
records' a2_expect entries are replaced by an asserted absence
({"record": "absent"}), which the checker now enforces by name, so a
record that comes back fails the census instead of being re-pinned.

Classified as an accepted baseline movement caused by #380: not a
P-037 implementation, not a #304 reopen, not evidence that the A2
contract holds. Canonical first-party call transport remains absent;
see docs/notes/p037-fact-shape-baseline-transition-380.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017znWXJbuLfmfZ7Un9GXzNp
Comment thread frontend/roslyn/samples/GuardedConsumeSample.cs
Comment thread frontend/roslyn/samples/GuardedConsumeSample.cs
…d limitation

The comment on IsDefiniteInBody claimed that declining a partial
release "never invents the opposite verdict". That holds for owned
locals, which become untracked escapes, but not for forwarded
parameters: the remaining `use` lets the existing inference derive
`no`, which is right for a false guard and wrong for a true or
forwarded one. The comment now says what #380 guarantees (no
fabricated `must`) and what it does not (INF-S2's `may`). Comment-only
change; the algorithm is untouched.

GuardedConsumeSample.ForwardDynamic forwards both its parameter and
the guard. INF-S3 would summarize it `may`; today it is `no`.
tests/test_guarded_consume.py pins `no` as a KNOWN LIMITATION, not as
acceptance, and fails with a message naming the direction if it
moves: `must` means the fabricated consume is back; `may` means a
canonical call fact reached the core, so the pin must be re-recorded
and #304's reopen condition 1 re-checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017znWXJbuLfmfZ7Un9GXzNp
@PhysShell
PhysShell merged commit fb06adc into main Sep 29, 2026
75 checks passed
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.

ConsumesParam flattens guarded release into unconditional handoff, causing false OWN009 and false must summaries

3 participants