fix(core): root 프로세스의 terminating 정적 게시를 웹 자가 치유로 위임

sudo 코어 업데이트(7.0.9→7.0.10 실서버)에서 자식 프로세스(신코드, root)의
terminating 게시가 소유권 복원 이후 시점에 실행되며 캐시 락 샤드 디렉토리
(storage/framework/cache/data/xx)와 병합 번들(storage/app/ext-bundles)을
root 소유로 남겼고, 이후 웹 프로세스의 캐시 쓰기가 그 샤드에 해시되는 순간
Permission denied 로 죽어 전면 500 이 됐다 (laravel.log 무기록 — 로거 도달 전).

normalizeOwnership 은 게시 트리(build/ext)만 다뤘다 — 게시의 간접 write-set
(캐시 락·번들 빌드)이 사각이었다. 뒤쫓아 chown 하는 대신 root 프로세스에서는
terminating 자동 게시를 예약하지 않고 다음 웹 렌더의 자가 치유(웹 계정)에
위임한다. 명시적 ext-static:publish 는 게이트 밖(운영자 책임 — 규정 §6).

- 시나리오 매니페스트에 process_user(web/root_cli) 축 신설 — uid 조합 실측
 부재가 이 결함을 통과시킨 구조적 원인 (기존 마커 전수에 web 부여)
- red 소급 증빙: 게이트 부재 커밋본에서 신규 케이스 실패 확인 → 15/15 green
This commit is contained in:
HeuJung committed 2026-08-25 17:37:24 +09:00
1 parent 5ba7a83597
commit 0bb33e3baa
9 files changed
+98 -17

No files matched your search

@@ -46,6 +46,9 @@ class ExtensionStaticCacheService
/** terminating 게시 예약 플래그 (프로세스당 1회) */
private static bool $publishScheduled = false;
/** 테스트 전용 — root 프로세스 판정 오버라이드 (null = 실판정) */
private static ?bool $rootProcessForTesting = null;
/** isPublished 요청당 메모이즈 (version => 존재 여부) */
private array $publishedMemo = [];
@@ -184,6 +187,18 @@ class ExtensionStaticCacheService
return;
}
// root 프로세스(sudo 코어 업데이트 등)에서는 예약하지 않는다 — 게시가 만드는
// 캐시 락 샤드 디렉토리(storage/framework/cache/data/xx)와 병합 번들
// (storage/app/ext-bundles)이 root 소유로 남아, 이후 웹 프로세스의 캐시
// 쓰기가 그 샤드에 해시되는 순간 Permission denied 로 죽는다 (실사례:
// sudo 업데이트 직후 전면 500). normalizeOwnership 은 게시 트리(build/ext)만
// 다루므로 storage 측 부수 산출물은 회피가 정답이다. 게시는 다음 웹 렌더의
// 자가 치유(웹 계정)가 수행하고, 명시적 `ext-static:publish` 커맨드는 이
// 게이트를 거치지 않는다 (운영자 책임 — 규정 문서 §6).
if (self::isRootProcess()) {
return;
}
self::$publishScheduled = true;
app()->terminating(static function (): void {
@@ -209,6 +224,33 @@ class ExtensionStaticCacheService
public static function resetPublishScheduleForTesting(): void
{
self::$publishScheduled = false;
self::$rootProcessForTesting = null;
}
/**
* 테스트 전용 — root 프로세스 판정을 강제합니다 (null 로 실판정 복귀).
*
* @param bool|null $isRoot 강제할 판정값
*/
public static function fakeRootProcessForTesting(?bool $isRoot): void
{
self::$rootProcessForTesting = $isRoot;
}
/**
* 현재 프로세스가 root(euid 0)로 실행 중인지 판정합니다.
*
* posix 확장이 없는 환경(Windows, 함수 비활성 호스팅)은 root 아님으로 본다.
*
* @return bool root 실행 여부
*/
private static function isRootProcess(): bool
{
if (self::$rootProcessForTesting !== null) {
return self::$rootProcessForTesting;
}
return function_exists('posix_geteuid') && posix_geteuid() === 0;
}
/**