Skip to content
Open
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
38 changes: 38 additions & 0 deletions docs/todo3004-sdk-recovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# ToDo3004: PassShare検査SDKの回収

元2997・親2994。cmd/runnerと実runner/service/profileは変更しない。
所有確認済み3rootの回収だけ。未使用の保護対象は保持中。

## 実装と検査(2026-10-19)

safe-deleteに固定所有記録の照合付きsdk-links退避を追加した。
plan/一般quarantineのリンク・Git・外部hardlink保護は変更しない。
13相対symlinkに加え、実物2rootでWindows生成の内部絶対junctionを発見した。
ジャンクションは走査せず、raw reparse SHA256と元の文字列を保持する。
退避後の絶対参照は元rootを指して切れるが、実体は同時に保存して照合する。
GoのEvalSymlinksではjunctionの最終実体を確認できなかったため、OSの
GetFinalPathNameByHandleWで移動前の最終パスを確認する。

Go race/vet/buildと合成fixtureで、相対symlink、内部junction、外部参照拒否、
変更拒否、一般planの拒否、有限receiptの拒否を確認した。
実物のinspect-sdk(読み取りだけ)も3root成功。service停止/PID0を別途確認済み。

| 所有ID | inventory(root除く) | 通常ファイルbytes |
| --- | ---: | ---: |
| 7155bdd37afc8e75e4dbf39509a5f18c | 7873 | 935313295 |
| c0d0c333fca8d97bab2fb8219b1ee1a8 | 7884 | 938846684 |
| 5eac22dc8041f9b3a471dbb392f86b99 | 7878 | 938945621 |

合計2,813,105,600 bytes。これは移動前の内容量であり、削除量や空き増加ではない。

## 再開と残作業

同じscheduler/todo-3004-passshare-sdk-2997ブランチで継続する。
検査用生成物はsafe-delete所有ID141fa64610dd2fafc76bbb1cd77e07b2。
この中のreceipts-goは読み取り検査用の原所有記録のコピーであり、正式記録を
更新していない。実回収時は配備済み道具の正規receipts-goを使う。

残作業: 別モデルレビュー、CI、PRマージ、並行変更照合後の配備、3回の正規退避、
元path不在/保存先全inventory/正式所有記録の確認、親2994/元2997へnote配送。
検査用生成物はplanの返却コマンドを別呼出に提出してfinishする。
通常の完了通知は行わない。失敗は正確な段階と残りを保存し、無限再試行しない。
28 changes: 28 additions & 0 deletions safe-delete/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,31 @@ go build -trimpath -o <所有付き検査フォルダー>/safe-delete.exe .

公開CIはWindowsで同じ全Go検査・vet・buildを実行する。
試験の共有名はt.TempDir内の架空データだけで、実物の所有IDを登録しない。

## ToDo3004: 回収済みSDKの有限な退避

`inspect-sdk --id <ID>` は読み取り検査、`quarantine --id <ID> --reason sdk-links`
は同じドライブの削除希望への保存を行う。対象はToDo2997が収集を終えた
7155bdd37afc8e75e4dbf39509a5f18c、c0d0c333fca8d97bab2fb8219b1ee1a8、
5eac22dc8041f9b3a471dbb392f86b99だけ。元の所有記録のチェックサムを固定し、
rootの識別番号・保護対象外のguard・合成fixture/checkoutと収集証拠を確認する。
退避中はfixture専用のclaimファイルを排他で保持し、サービス停止/PID0を再確認する。
実runnerやサービスprofileには触れない。

13個の相対ファイルsymlinkはruntime/sdk/bootstrap/binの決まった名前だけ許可し、
reparse tag/relative flag、リンク自身の識別情報、文字列rustup.exe、解決後の同一
ディレクトリー内の通常ファイルを検査する。他のreparseと外部hardlinkは拒否する。
Windowsが作ったruntime/home内のINetCache/Content.IE5だけは、同じhome内のIEを
指すmount-point tagと絶対targetを照合して保存する。走査では追跡しない。
絶対targetの文字列を変更せず保管するため、退避後は元pathを指して参照が切れる。
移動後はIEディレクトリー自身の保存済み識別番号と全内容を照合する。
汎用のGit保護は維持し、この退避に限り2個の合成Git領域を全内容保存の対象にする。
元のソース/controlはGitに保持済みで、これは実製品のcheckoutではない。

全inventoryの境界とhardlinkを検査してから通常ファイルをSHA256で照合する。
移動前に識別番号・相対名・属性・サイズ・時刻・リンク数・target・全ハッシュを
preservation.jsonへ保存し、直前と移動後にも同じ情報を比較する。
通常のMove-Itemだけを使い、元path不在と全内容保存を確認した後に所有記録を
quarantinedへ更新する。削除量ではなく退避量を報告する。
検査失敗時の別経路削除や自動再移動は行わず、両方の場所と失敗段階を保持する。
同一ユーザーによる敵対的な並行置換を防ぐセキュリティ境界ではない。
5 changes: 5 additions & 0 deletions safe-delete/gemini_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ type geminiEntry struct {
Attributes, Links, SizeHigh, SizeLow uint32
Created, Written syscall.Filetime
Hash string
Target string `json:",omitempty"`
}

func (s *geminiScope) measure(root string) (measured, error) {
return (config{MaxEntries: 10000}).measureWithShared(root, s)
}

func geminiMetadata(i syscall.ByHandleFileInformation) geminiEntry {
Expand Down
45 changes: 38 additions & 7 deletions safe-delete/safe_delete_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ func checked(value string) (string, error) {
return p, nil
}
func fileIdentity(path string) (syscall.ByHandleFileInformation, error) {
i, err := rawIdentity(path)
if err == nil && i.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return i, fail("LINK_OR_REPARSE_POINT")
}
return i, err
}

// rawIdentity is used only by the finite SDK preservation scanner; callers
// must validate the reparse tag and target before accepting a link.
func rawIdentity(path string) (syscall.ByHandleFileInformation, error) {
u, err := syscall.UTF16PtrFromString(path)
if err != nil {
return syscall.ByHandleFileInformation{}, fail("INVALID_PATH")
Expand All @@ -150,9 +160,6 @@ func fileIdentity(path string) (syscall.ByHandleFileInformation, error) {
if syscall.GetFileInformationByHandle(h, &info) != nil {
return info, fail("IDENTITY_READ_FAILED")
}
if info.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return info, fail("LINK_OR_REPARSE_POINT")
}
return info, nil
}
func fileID(i syscall.ByHandleFileInformation) uint64 {
Expand Down Expand Up @@ -461,7 +468,10 @@ func driveSpace() []map[string]any {
return result
}
func (c config) quarantine(id, reason string) (result map[string]any, err error) {
var scope *geminiScope
if reason == "sdk-links" {
return c.quarantineSDK(id)
}
var scope preservationScope
if reason == "hardlink" {
scope, err = c.geminiRecovery(id)
if err != nil {
Expand All @@ -470,7 +480,14 @@ func (c config) quarantine(id, reason string) (result map[string]any, err error)
}
return c.quarantineScoped(id, reason, scope)
}
func (c config) quarantineScoped(id, reason string, scope *geminiScope) (result map[string]any, err error) {

type preservationScope interface {
measure(string) (measured, error)
snapshot(string) (map[string]geminiEntry, error)
verify(string, map[string]geminiEntry, receipt) error
}

func (c config) quarantineScoped(id, reason string, scope preservationScope) (result map[string]any, err error) {
text, ok := reasons[reason]
if !ok {
return nil, fail("INVALID_REASON")
Expand All @@ -479,7 +496,12 @@ func (c config) quarantineScoped(id, reason string, scope *geminiScope) (result
if err != nil {
return nil, err
}
m, err := c.measureWithShared(r.Path, scope)
var m measured
if scope == nil {
m, err = c.measure(r.Path)
} else {
m, err = scope.measure(r.Path)
}
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -600,7 +622,7 @@ func appendReason(path, text string) error {
}
func main() {
if len(os.Args) < 2 || os.Args[1] == "--help" {
fmt.Println("safe-delete create --project NAME [--content NAME] [--root PATH]\nsafe-delete plan|finish --id ID\nsafe-delete quarantine --id ID --reason policy|windows|locked|attributes|size|hardlink\nhardlink is restricted to ToDo2307's fixed receipt and media.\nDeletion plans must be submitted separately to the caller's ordinary exec tool.")
fmt.Println("safe-delete create --project NAME [--content NAME] [--root PATH]\nsafe-delete plan|finish --id ID\nsafe-delete inspect-sdk --id ID\nsafe-delete quarantine --id ID --reason policy|windows|locked|attributes|size|hardlink|sdk-links\nhardlink is restricted to ToDo2307's fixed receipt and media. sdk-links preserves only ToDo3004's three collected SDK roots.\nDeletion plans must be submitted separately to the caller's ordinary exec tool.")
return
}
c := defaultConfig()
Expand All @@ -621,6 +643,15 @@ func main() {
result, err = c.create(*root, *project, *content)
case "plan":
result, err = c.plan(*id)
case "inspect-sdk":
var r receipt
r, err = c.sdkRecovery(*id)
if err == nil {
err = sdkCollected(r.Path, *id)
}
if err == nil {
result, err = (&sdkScope{source: r.Path}).measure(r.Path)
}
case "finish":
result, err = c.finish(*id)
case "quarantine":
Expand Down
Loading
Loading