Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
e525fac
fix(acquisition): bind commercial source records
seonghobae Aug 31, 2026
0f83739
docs(acquisition): align transfer source binding contract
seonghobae Sep 1, 2026
f204a65
docs(acquisition): fix source binding examples
seonghobae Sep 1, 2026
e466a31
docs(acquisition): retain evidence owner requirement
seonghobae Sep 1, 2026
6a1093c
docs(acquisition): complete revenue source binding contract
seonghobae Sep 1, 2026
b183bbb
docs(acquisition): keep evidence freshness examples current
seonghobae Sep 1, 2026
90e58c6
merge(main): converge commercial evidence after #525
seonghobae Sep 1, 2026
1e83376
test(acquisition): reject hardlinked retained evidence
seonghobae Sep 1, 2026
6a9fe82
fix(acquisition): reject hardlinked retained evidence
seonghobae Sep 1, 2026
f4b096a
docs(acquisition): refresh protected readiness snapshot
seonghobae Sep 1, 2026
4e24e00
test(acquisition): reject retained evidence close failure
seonghobae Sep 1, 2026
07aab9a
fix(acquisition): fail closed on retained evidence close
seonghobae Sep 1, 2026
e32755e
test(acquisition): align close failure contract
seonghobae Sep 1, 2026
8747679
test(acquisition): reject post-close retained-path replacement
seonghobae Sep 1, 2026
4db70c2
fix(acquisition): revalidate retained path after close
seonghobae Sep 1, 2026
6f69acc
test(acquisition): preserve concurrent failed-output replacement
seonghobae Sep 1, 2026
428764a
fix(acquisition): avoid pathname-delete race on failed evidence
seonghobae Sep 1, 2026
aa8bc52
test(acquisition): expect descriptor-bound failure neutralization
seonghobae Sep 1, 2026
a0d28d0
test(acquisition): retain failed output instead of pathname delete
seonghobae Sep 1, 2026
61a321e
test(acquisition): bound FIFO cleanup race
seonghobae Sep 1, 2026
22764ce
fix(acquisition): make failed-evidence cleanup nonblocking
seonghobae Sep 1, 2026
7ef8822
test(acquisition): keep FIFO regression hermetic
seonghobae Sep 1, 2026
8777585
test(acquisition): require nonblocking cleanup capability
seonghobae Sep 1, 2026
8fa71e5
fix(acquisition): require nonblocking cleanup capability
seonghobae Sep 1, 2026
e3fea35
test(acquisition): model nonblocking cleanup capability
seonghobae Sep 1, 2026
63372c1
test(acquisition): expose nonblocking adapter capability
seonghobae Sep 1, 2026
253da4f
test(acquisition): carry nonblocking parent-race capability
seonghobae Sep 1, 2026
9d099a5
test(acquisition): preserve nonblocking cleanup semantics
seonghobae Sep 1, 2026
b8cc388
test(acquisition): carry nonblocking staging capability
seonghobae Sep 1, 2026
19db095
test(acquisition): add nonblocking replacement capability
seonghobae Sep 1, 2026
e6da3ed
test(acquisition): provide nonblocking atomic adapter capability
seonghobae Sep 1, 2026
2c5d637
test(operations): expose nonblocking report capability
seonghobae Sep 1, 2026
e10a482
Merge 5aad3e410703faaf52882e2f33fadd25d217bcdd into 2c5d6378f4842008e…
seonghobae Sep 1, 2026
8b67a93
test(acquisition): align atomic-failure cleanup with descriptor-bound…
seonghobae Sep 1, 2026
eaebeec
fix(test): keep descriptor-bound failure evidence non-authoritative
seonghobae Sep 1, 2026
545c247
Merge main into fix/526-continue: combine commercial-evidence PR #526…
claude Sep 2, 2026
fbed32c
test: cover neutralize-cleanup and afterClosePath fail-closed branches
claude Sep 2, 2026
85f14cd
docs: log coverage-gate regression test additions in CHANGELOG
claude Sep 2, 2026
e4110a1
Merge remote-tracking branch 'origin/main' into fix/acquisition-sourc…
claude Sep 2, 2026
722c06d
fix(acquisition): make existing-target verification read non-blocking
claude Sep 2, 2026
630612c
Merge remote-tracking branch 'origin/main' into HEAD
claude Sep 3, 2026
ce40546
Merge protected main into acquisition evidence lane
seonghobae Sep 4, 2026
e32e7e7
Restack acquisition evidence on protected reviewer foundation
seonghobae Sep 6, 2026
81ef8b7
merge(context-fabric): restack acquisition evidence on protected cont…
seonghobae Sep 6, 2026
21ead9f
fix(acquisition): converge evidence authority on current main
seonghobae Sep 6, 2026
bd4c907
merge: restack acquisition evidence after #527 trust integration
seonghobae Sep 6, 2026
399d51d
merge(main): restack acquisition evidence after protected trust integ…
seonghobae Sep 6, 2026
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# Changelog

## Unreleased
- `writeAcquisitionPrivateFile`의 기존 대상 사전-교체 검증 read(`existingDescriptor` open)에 `O_NONBLOCK`을 추가해 fail-closed를 강화한다. 이 open은 이미 필수 filesystem capability로 `O_NONBLOCK`을 검증했지만 실제로는 사용하지 않아, 로컬 권한을 가진 행위자가 사전 `lstatSync` 정규 파일 확인과 이 open 사이에 대상 경로를 FIFO로 교체하면 writer가 나타날 때까지 무한정 블로킹해 writer lease를 계속 점유할 수 있었다. `O_NONBLOCK`은 정규 파일에는 영향이 없고, FIFO에서는 open이 즉시 반환되어 이어지는 descriptor 타입 검증이 그대로 fail-closed로 거부한다. 회귀 테스트(`test/acquisition-private-output-existing-target-nonblocking.test.ts`)와 기존 open-flags 계약 테스트 갱신으로 고정했다.
- `readStableFile`의 close-후 재검증 단계(`afterClosePath` lookup 실패)와 `writeAcquisitionPrivateFile`의 cleanup-시점 `O_NONBLOCK` 소실 분기에 대한 fail-closed 회귀 테스트를 추가해 `scripts/lib/acquisition-data-room-integrity.mjs`/`scripts/lib/acquisition-private-output.mjs`의 100% coverage 게이트를 복구한다. 동작 변화는 없다.
- Noema reviewer의 strict changed-file evidence를 historical 12-file prefix에서 canonical 80-file CodeGraph scope와 일치시켰다. 13–80 file PR은 선택된 모든 current-head file context를 유지하고 81개 이상은 기존처럼 실패-폐쇄하며, local CodeGraph fallback의 `HOME`·`TEMP`·`TMP`·`TMPDIR`은 ambient host path를 상속하지 않고 실행마다 새 private temporary directory로 격리한다.
- Workflow / Task Execution은 untrusted DAG를 execution/plan identity에 결합한 detached immutable snapshot으로 승인하고, validated array bounds 안에서만 task/dependency/state evidence를 읽는다. runnable 선택은 cross-execution·foreign·duplicate·non-canonical evidence, admitted concurrency를 초과한 running state, 성공하지 않은 prerequisite 뒤에 존재하는 causally impossible executed state를 실패-폐쇄하며, 선택 결과는 reservation이나 side-effect authority가 아닌 후보임을 명시한다. Agent Runtime lifecycle·State & Checkpoint·Workflow admission은 null·throwing accessor·revoked proxy 같은 malformed runtime input의 임의 JavaScript 예외를 각 bounded-context domain error로 정규화한다.
- State & Checkpoint admission은 accepted/replay 결과와 내부 checkpoint를 모두 caller-owned alias에서 분리한 frozen snapshot으로 반환한다. TypeScript `readonly`만으로는 막을 수 없는 JavaScript 런타임 alias mutation이 승인된 checkpoint authority나 `accepted`/`replay` 분류를 사후 변경하지 못하도록 실패-폐쇄한다.
- Noema의 필수 PR 워크플로 `ci`, `reviewer-ci`, `patch-validator-image`를 부동 `ubuntu-latest` 대신 명시적 `ubuntu-24.04` GitHub-hosted runner에 고정하고, 인용 여부와 무관하게 `ubuntu-latest` 회귀를 탐지하는 계약 테스트를 추가해 pre-checkout runner-assignment stall의 repository-owned selector 원인을 제거한다. 중앙 `Security Scan`의 runner/control-plane 권한은 별도 `.github` owner 경계에 유지한다.
- 비공개 취약점 보고 감사가 16 KiB 응답 상한, bounded stream 취소, canonical repository/source identity의 독립 검증, SHA-1/SHA-256 exact revision, symlink·retained-path 보호를 실패-폐쇄로 강제한다. 이 감사 결과는 live private reporting 활성화, notification staffing, 실제 advisory 대응 또는 release/deployment 완료 증거를 대신하지 않는다.
- External scheduler evidence audits now retain source authority through final report publication: reports are owner-only, no-follow, exclusive one-shot receipts, so a concurrent rename cannot move the accepted source inode onto the report pathname and have it replaced. Source/report path and inode alias checks, single-link retained-source validation, and Unicode control sanitization remain fail closed.
- revenue/transfer acquisition evidence의 `source_documents`를 임의 문자열 label 대신 stable retained artifact의 `{path, sha256}` binding으로 검증한다. Digest 일치는 보존된 bytes의 무결성만 증명하며 CRM·계약·매출·법률 기록의 진실성이나 승인 권한은 계속 별도 buyer evidence로 요구한다.
- production runtime credential envelope parsing을 fail-closed로 강화한다. GitHub App PKCS#1 key의 canonical PKCS#8 변환은 유지하되, bare carriage return처럼 비정규 body bytes가 포함된 PKCS#8 PEM은 readiness/import 단계의 암묵적 정규화에 넘기지 않고 즉시 거부해 malformed secret이 ready 상태로 승인되지 않게 한다.
- acquisition tracked-byte 인증이 descriptor에서 읽은 bytes를 Git blob framing으로 Node 표준 crypto에서 직접 해시해, 파일마다 `git hash-object` subprocess를 만들던 대형 checkout 병목을 제거한다. exact tree inventory는 Git 2.36 전용 `ls-tree --format` 대신 호환되는 기본 NUL 형식을 사용하며, object ID, SHA-1/SHA-256 저장소, no-follow·descriptor identity·byte limit 실패-폐쇄 계약은 유지한다. dependency-license inventory가 실제로 소비한 `package-lock.json` bytes도 pinned source commit의 Git blob과 직접 대조해 transient file swap을 차단한다. 실패한 audit stage 뒤에도 source를 다시 인증한 다음 원래 child status로 종료하므로 failure evidence가 stale revision으로 남지 않으며, release·publication·deployment evidence producer와 acquisition consumer는 canonical SHA-1/SHA-256 commit identity를 동일하게 지원한다.
- `acquisition:audit`가 POSIX shell 문법 없이 Node 오케스트레이터로 exact HEAD 기반 단일 기본 output directory를 manifest·integrity·readiness·deployment 단계에 전달해 Windows에서도 새 manifest를 같은 실행에서 소비하며, 기존 `NOEMA_ACQUISITION_AUDIT_OUTPUT_DIR`·`NOEMA_DATA_ROOM_OUTPUT_DIR` 경로 override는 유지한다.
Expand Down
2 changes: 1 addition & 1 deletion docs/TRACEABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Each arrow is a separate authority. Success at an earlier stage cannot fabricate
| Credential/security coverage truth | protected main | protected `src/index.ts`, `docs/TEST_STRATEGY.md` and coverage contracts | exact configured 100% statement/branch/function/line gates; no broad credential/security V8-ignore contract | current protected-main CI remains observation-scoped | Implemented on protected main |
| Patch-validator image supply chain | issue #66 + protected implementation | `Dockerfile.patch-validator`, image workflow, validator runtime/profile, SBOM/scanner/receipt validators | exact build/runtime/smoke/SBOM/vulnerability/receipt/final-head verification | protected-main operational receipt and later publication/signing/activation evidence | Source/runtime/supply-chain implementation is integrated on protected main; later operational/publication authority remains separate |
| Licensing/IP authority | licensing/IP contract | rights/evidence validators | duplicate-key/UTF-8/exact-artifact and rights-metadata tests | owner/legal grant and transfer evidence | Technical controls exist; legal authority external |
| Release/acquisition readiness | release/provenance/acquisition contracts | release verification and evidence scripts | exact-source package/SBOM/provenance/readiness tests | immutable release/deployment/customer/revenue/legal evidence | Incomplete; no readiness claim from docs alone |
| Release/acquisition readiness | release/provenance/acquisition contracts | release verification and evidence scripts, digest-bound revenue/transfer source documents | exact-source package/SBOM/provenance/readiness and retained-source byte-integrity tests | immutable release/deployment/customer/revenue/legal authority | Technical byte binding implemented; commercial/legal authenticity remains external |

## 3. Live governance traceability

Expand Down
28 changes: 20 additions & 8 deletions docs/acquisition-readiness-2b.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,13 @@ Product Design 기준으로 구매자와 파일럿 고객이 제품 가치를
- security evidence: `artifacts/security/security-validation-evidence.json` (`npm run security:evidence`로 단독 검증)
- production pilot log: `docs/pilot-readiness-log.md` 또는 `NOEMA_PILOT_LOG_PATH`
- saleable readiness evidence: `artifacts/saleable-readiness/<YYYYMMDD>/goal-audit.json`
- revenue/transfer evidence는 `owner`, `source_documents`, 최근 `updated_at`을 포함해야 한다.
- revenue/transfer evidence는 `owner`, 최근 `updated_at`, 그리고 1~32개의 retained `{path, sha256}` 레코드로 구성된 `source_documents`를 포함해야 한다. `path`는 canonical repository-relative evidence 경로여야 하고 `sha256`은 그 보존 파일의 64-hex SHA-256이어야 한다. 이 digest 검증은 보존된 bytes의 무결성만 증명하며 CRM·계약·매출·법률 기록의 진실성이나 승인 권한을 대신하지 않는다.
- `updated_at`은 기본 45일 이내 증빙이어야 하며, 필요 시 `NOEMA_ACQUISITION_EVIDENCE_MAX_AGE_DAYS`로 조정한다.
- Strategic pipeline route는 `buyer_due_diligence_qna`에 구매자별 보안/운영 실사 Q&A 로그 경로를 1개 이상 포함해야 한다.
- production pilot log는 production HTTPS `NOEMA URL`, `증빙 출처: production`, KPI threshold, trace sample, support channel, 계약/매출 증빙 경로가 있는 완료 항목 1건 이상을 요구한다.
- 작성 템플릿은 `docs/evidence-templates/revenue-evidence.example.json`, `docs/evidence-templates/transfer-evidence.example.json`에 둔다. 템플릿은 `artifacts/acquisition/*.json`으로 복사한 뒤 placeholder를 실제 owner/source/evidence 값으로 교체해야 한다. `replace-with-*`, `.example.json`, `docs/evidence-templates/` 값은 `npm run acquisition:audit`에서 evidence로 인정하지 않는다.

예시는 다음과 같다.
예시는 형식 설명용이다. 실제 제출에서는 예시 digest를 해당 retained bytes의 SHA-256으로 교체하고, `updated_at`도 제출 시점의 freshness window(기본 45일) 안에 있는 실제 증빙 갱신일로 반드시 교체해야 한다.
Comment thread
seonghobae marked this conversation as resolved.

```json
{
Expand All @@ -160,11 +160,17 @@ Product Design 기준으로 구매자와 파일럿 고객이 제품 가치를
"crm:noema-enterprise-security-qna"
],
"customer_concentration_top1": 0.5,
"updated_at": "2026-07-02",
"updated_at": "2026-09-01",
"owner": "finance",
"source_documents": [
"crm:noema-arr-report",
"contracts/noema-paid-customers.pdf"
{
"path": "artifacts/acquisition/source-records/noema-arr-report.json",
"sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
{
"path": "artifacts/acquisition/source-records/noema-paid-customers.pdf",
"sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
}
]
}
```
Expand All @@ -178,11 +184,17 @@ Product Design 기준으로 구매자와 파일럿 고객이 제품 가치를
"secrets_rotation_plan": "pass",
"owner_transfer_plan": "pass",
"privacy_review": "pass",
"updated_at": "2026-07-02",
"updated_at": "2026-09-01",
"owner": "legal",
"source_documents": [
"docs/buyer-due-diligence-index.md",
"legal/noema-transfer-review.pdf"
{
"path": "legal/noema-transfer-review.pdf",
"sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
},
{
"path": "legal/noema-ip-assignment-register.pdf",
"sha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
}
]
}
```
Expand Down
4 changes: 2 additions & 2 deletions docs/buyer-due-diligence-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Production 파일럿 로그는 `npm run acquisition:audit`에서도 직접 검

## Commercial

`artifacts/acquisition/revenue-evidence.json`에는 `owner`, `source_documents`, 기본 45일 이내 `updated_at`있어야 한다.
`artifacts/acquisition/revenue-evidence.json`에는 `owner`, 기본 45일 이내 `updated_at`, 그리고 1~32개의 retained source binding으로 구성된 `source_documents`가 있어야 한다. 각 항목은 canonical repository-relative `path`와 그 보존 파일 bytes의 64-hex `sha256`을 담는 `{path, sha256}` 레코드여야 하며 placeholder나 template 경로는 인정하지 않는다. SHA-256 일치는 byte integrity일 뿐 CRM·계약·지급·법률 기록의 진실성 또는 승인 권한은 별도 authoritative evidence다.
작성 템플릿은 `docs/evidence-templates/revenue-evidence.example.json`이다. `replace-with-*`, `.example.json`, `docs/evidence-templates/` 값은 evidence로 인정하지 않는다.

| 항목 | Evidence | 상태 |
Expand All @@ -100,7 +100,7 @@ Production 파일럿 로그는 `npm run acquisition:audit`에서도 직접 검

## Transfer

`artifacts/acquisition/transfer-evidence.json`에는 `owner`, `source_documents`, 기본 45일 이내 `updated_at`있어야 한다.
`artifacts/acquisition/transfer-evidence.json`에는 `owner`, 기본 45일 이내 `updated_at`, 그리고 1~32개의 retained source binding으로 구성된 `source_documents`가 있어야 한다. 각 항목은 canonical repository-relative `path`와 그 보존 파일 bytes의 lowercase/uppercase 64-hex `sha256`을 담는 `{path, sha256}` 레코드여야 하며 placeholder나 template 경로는 인정하지 않는다. SHA-256 일치는 보존 bytes의 무결성만 증명하고, 법률·IP·계정 이전 기록의 진실성이나 승인 권한은 별도 authoritative evidence로 확인해야 한다.
작성 템플릿은 `docs/evidence-templates/transfer-evidence.example.json`이다. `replace-with-*`, `.example.json`, `docs/evidence-templates/` 값은 evidence로 인정하지 않는다.

| 항목 | Evidence | 상태 |
Expand Down
10 changes: 8 additions & 2 deletions docs/evidence-templates/revenue-evidence.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@
"updated_at": "replace-with-YYYY-MM-DD",
"owner": "replace-with-finance-or-sales-owner",
"source_documents": [
"replace-with-crm-arr-report",
"replace-with-contract-or-loi-path"
{
"path": "replace-with-retained-crm-arr-report-path",
"sha256": "replace-with-retained-crm-arr-report-sha256"
},
{
"path": "replace-with-retained-contract-or-loi-path",
"sha256": "replace-with-retained-contract-or-loi-sha256"
}
]
}
12 changes: 9 additions & 3 deletions docs/evidence-templates/transfer-evidence.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@
"updated_at": "replace-with-YYYY-MM-DD",
"owner": "replace-with-legal-or-security-owner",
"source_documents": [
"replace-with-license-review-path",
"replace-with-transfer-runbook-or-approval-path"
{
"path": "replace-with-retained-license-review-path",
"sha256": "replace-with-retained-license-review-sha256"
},
{
"path": "replace-with-retained-transfer-approval-path",
"sha256": "replace-with-retained-transfer-approval-sha256"
}
],
"licensing_ip": {
"owner_legal_decision": {
Expand Down Expand Up @@ -61,4 +67,4 @@
]
}
}
}
}
14 changes: 11 additions & 3 deletions scripts/acquisition-readiness-audit.mjs
Comment thread
seonghobae marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { hasDuplicateJsonObjectKeys } from "./normalize-commercial-readiness-evi
const fatalUtf8Decoder = new TextDecoder("utf-8", { fatal: true });
const isoDateOrTimestampRegex = /^(\d{4}-\d{2}-\d{2})(?:T(?:[01]\d|2[0-3]):\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2}))?$/;
const MAX_ISO_UTC_OFFSET_MS = 14 * 60 * 60 * 1000;
const MAX_SOURCE_DOCUMENTS = 32;
const now = new Date().toISOString();
const configuredOutputDir = process.env.NOEMA_ACQUISITION_AUDIT_OUTPUT_DIR;
if (configuredOutputDir) {
Expand Down Expand Up @@ -198,8 +199,15 @@ function validateEvidenceMetadata(value) {
} else if (isPlaceholderEvidence(value.owner)) {
failures.push("owner cannot be a placeholder");
}
const sourceDocuments = validateEvidenceRefs(value.source_documents, "source_documents");
failures.push(...sourceDocuments.failures);
if (!Array.isArray(value.source_documents) || value.source_documents.length === 0) {
failures.push("source_documents must contain at least one retained artifact binding");
} else if (value.source_documents.length > MAX_SOURCE_DOCUMENTS) {
failures.push(`source_documents must contain at most ${MAX_SOURCE_DOCUMENTS} artifact bindings`);
} else {
value.source_documents.forEach((document, index) => {
validateDigestBoundArtifact(document, `source_documents[${index}]`, failures);
});
Comment thread
seonghobae marked this conversation as resolved.
}
if (!updatedAt || Number.isNaN(updatedAtMs)) {
failures.push("updated_at must be an ISO date or timestamp");
} else if (updatedAtMs > futureBoundaryMs) {
Expand Down Expand Up @@ -864,4 +872,4 @@ if (!output.passed) {
}
process.exit(1);
}
}
}
33 changes: 25 additions & 8 deletions scripts/lib/acquisition-data-room-integrity.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ function isSafeRegularMetadata(metadata, maximumBytes) {
&& typeof metadata.isSymbolicLink === "function"
&& metadata.isFile()
&& !metadata.isSymbolicLink()
&& (metadata.nlink === undefined || metadata.nlink === 1)
&& Number.isSafeInteger(metadata.size)
&& metadata.size >= 0
&& metadata.size <= maximumBytes,
Expand All @@ -203,13 +204,16 @@ function sameIdentity(left, right) {
}

/**
* Read a bounded regular file through O_NOFOLLOW and require path/descriptor
* identity to remain stable before and after the complete read. The returned
* bytes are suitable for hashing or fatal UTF-8 decoding; unsafe evidence is
* represented as null rather than partially trusted data.
* Read a bounded single-link regular file through O_NOFOLLOW and require path/descriptor
* identity to remain stable before, during, and after the complete read and descriptor
* close. The returned bytes are suitable for hashing or fatal UTF-8 decoding; unsafe
* evidence is represented as null rather than partially trusted data. Injectable test
* metadata may omit nlink; real filesystem metadata must report exactly one link.
*/
export function readStableFile(path, maximumBytes = MAX_DATA_ROOM_EVIDENCE_BYTES, fileSystem = defaultFileSystem) {
let descriptor = null;
let opened = null;
let result = null;
try {
if (!Number.isSafeInteger(maximumBytes) || maximumBytes <= 0) {
return null;
Expand All @@ -224,7 +228,7 @@ export function readStableFile(path, maximumBytes = MAX_DATA_ROOM_EVIDENCE_BYTES
return null;
}
descriptor = fileSystem.openSync(path, readOnly | noFollow);
const opened = fileSystem.fstatSync(descriptor);
opened = fileSystem.fstatSync(descriptor);
if (!isSafeRegularMetadata(opened, maximumBytes) || !sameIdentity(before, opened)) {
return null;
}
Expand All @@ -248,19 +252,32 @@ export function readStableFile(path, maximumBytes = MAX_DATA_ROOM_EVIDENCE_BYTES
if (!sameIdentity(opened, afterDescriptor) || !sameIdentity(opened, afterPath)) {
return null;
}
return bytes;
result = bytes;
} catch {
return null;
} finally {
if (descriptor !== null) {
try {
fileSystem.closeSync(descriptor);
} catch {
// A failed close cannot make evidence more trustworthy; the read result
// is already bounded and callers remain fail-closed on validation.
result = null;
}
}
if (result !== null && opened !== null) {
try {
const afterClosePath = fileSystem.lstatSync(path);
if (
!isSafeRegularMetadata(afterClosePath, maximumBytes)
|| !sameIdentity(opened, afterClosePath)
) {
result = null;
}
} catch {
result = null;
}
}
}
return result;
Comment thread
seonghobae marked this conversation as resolved.
}

function canonicalRelativePath(rootDir, candidate) {
Expand Down
Loading