openfsd-client: hybrid shadow PE launch

Phase 1 ephemeral launch: patch a temp PE copy while cwd remains
the install root so DLLs resolve; durable config stays default.
This commit is contained in:
Reese Norris
2026-07-28 23:15:26 -04:00
parent eb01f3afbf
commit 26b9e299f5
7 changed files with 1022 additions and 6 deletions

View File

@@ -45,6 +45,11 @@ Shared flags for plan|apply|health|launch:
--client ID Client adapter id (default vpilot)
--profiles-dir DIR Optional override profile directory (YAML)
Launch-only flags:
--ephemeral Phase 1 hybrid shadow PE: patch a temp PE copy, cwd=install
root (DLLs resolve from install); durable config rewrite
remains default. Install PE stays stock.
Readiness honesty:
Default JWT path /api/v1/fsd-jwt allows max host 12 characters for in-place #US
patch (budget 35 runes). Use --prefer-short-jwt for /j (max host 25) if the
@@ -160,6 +165,7 @@ type endpointFlags struct {
install string
client string
profilesDir string
ephemeral bool // launch --ephemeral (hybrid shadow PE)
}
func parseEndpointFlags(name string, args []string, stderr io.Writer) (*endpointFlags, []string, int) {
@@ -176,6 +182,8 @@ func parseEndpointFlags(name string, args []string, stderr io.Writer) (*endpoint
fs.StringVar(&ef.install, "install", "", "client install directory")
fs.StringVar(&ef.client, "client", "vpilot", "client id")
fs.StringVar(&ef.profilesDir, "profiles-dir", "", "optional profiles directory")
// Launch-only; harmless no-op if passed to other subcommands.
fs.BoolVar(&ef.ephemeral, "ephemeral", false, "hybrid shadow PE launch (temp PE, cwd=install, durable config)")
if err := fs.Parse(args); err != nil {
return nil, nil, ExitUsage
}
@@ -299,10 +307,40 @@ func cmdLaunch(args []string, stdout, stderr io.Writer) int {
fmt.Fprintf(stderr, "no adapter for %q\n", install.ClientID)
return ExitUsage
}
if !ef.ephemeral {
launchArgs := a.LaunchArgs(install, ep)
pe := clientinject.AbsPrimaryPE(install)
fmt.Fprintf(stdout, "exec: %s %s\n", pe, strings.Join(launchArgs, " "))
fmt.Fprintf(stdout, "(launch is dry-print only without --ephemeral; use --ephemeral for Phase 1 hybrid shadow PE)\n")
return ExitOK
}
// Phase 1 hybrid shadow PE launch (Appendix B).
plan, err := eng.Plan(context.Background(), install, ep)
if err != nil {
return mapPlanErr(err, stderr)
}
printPlan(stdout, plan)
if len(plan.Blockers) > 0 {
fmt.Fprintf(stderr, "plan has blockers; refuse ephemeral launch\n")
return ExitPlanBlockers
}
launchArgs := a.LaunchArgs(install, ep)
pe := clientinject.AbsPrimaryPE(install)
fmt.Fprintf(stdout, "exec: %s %s\n", pe, strings.Join(launchArgs, " "))
fmt.Fprintf(stdout, "(launch is dry-print only in headless CLI; start the client with these args from the install directory)\n")
// Prefer plan launch_flag args when present.
if fromPlan := clientinject.LaunchArgsFromPlan(plan); len(fromPlan) > 0 {
launchArgs = fromPlan
}
fmt.Fprintf(stdout, "ephemeral shadow launch: cwd=%s pe=temp-copy durable_config=true\n", install.RootDir)
fmt.Fprintf(stdout, "launch args: %s\n", strings.Join(launchArgs, " "))
session, err := eng.LaunchShadow(context.Background(), plan, launchArgs, clientinject.ShadowLaunchConfig{})
if err != nil {
fmt.Fprintf(stderr, "ephemeral launch: %v\n", err)
return ExitApplyFailed
}
if session != nil {
fmt.Fprintf(stdout, "ephemeral launch finished (temp_kept=%v install_pe_stock_fp=%s)\n",
session.KeptTemp(), session.InstallPEFingerprint)
}
return ExitOK
})
}

View File

@@ -63,3 +63,23 @@ func TestRun_Detect(t *testing.T) {
}
// May find zero installs on this machine; just ensure no crash.
}
func TestRun_HelpMentionsEphemeral(t *testing.T) {
var out, errBuf bytes.Buffer
code := Run([]string{"help"}, &out, &errBuf)
if code != ExitOK {
t.Fatalf("code=%d err=%s", code, errBuf.String())
}
if !strings.Contains(out.String(), "--ephemeral") {
t.Fatalf("help missing --ephemeral: %s", out.String())
}
}
func TestRun_LaunchDryPrintWithoutEphemeral(t *testing.T) {
// Missing install → usage; ensures flag parsing accepts launch without ephemeral.
var out, errBuf bytes.Buffer
code := Run([]string{"launch", "--web-base", "https://x.test", "--fsd-host", "x.test"}, &out, &errBuf)
if code != ExitUsage {
t.Fatalf("code=%d err=%s", code, errBuf.String())
}
}

View File

@@ -1,12 +1,16 @@
// Package clientinject provides the openfsd Client Setup engine: version-pinned
// profiles, adapter plan/apply/revert, transactional .openfsd-bak backups, and
// running-process preflight for on-disk client reconfiguration.
// profiles, adapter plan/apply/revert, transactional .openfsd-bak backups,
// running-process preflight, and Phase 1 hybrid shadow PE launch.
//
// Pure subpackages (stdlib only):
// - pepatch — PE/binary overwrite helpers
// - cilus — CLR #US heap encode/decode
// - vpilotconfig — vPilot 3DES config crypto + XML field rewrite
//
// Hybrid shadow launch (LaunchShadow / --ephemeral): patch a temp copy of the
// PE while cwd remains the install root so DLLs resolve; durable config rewrite
// is the default; install PE stays stock.
//
// This package must not import server, web, afv, db, postoffice, session,
// cluster, sweatbox, metar, auth, or serviceapi.
package clientinject

View File

@@ -69,8 +69,12 @@ func (f *FakeAdapter) Apply(ctx context.Context, plan *Plan, w FileWriter) error
return errors.New("fake apply boom")
}
for _, m := range plan.Mutations {
// Launch flags are plan-only (no disk write), same as production adapters.
if m.Kind == MutLaunchFlag {
continue
}
path := m.TargetRel
if !filepath.IsAbs(path) {
if path != "" && !filepath.IsAbs(path) {
path = filepath.Join(plan.Install.RootDir, path)
}
switch d := m.Detail.(type) {

View File

@@ -0,0 +1,63 @@
package clientinject
import (
"context"
"fmt"
"os"
"os/exec"
)
// ProcessRunner executes a client binary. Injectable for tests so shadow
// launch can be exercised without a real PE on the host.
type ProcessRunner interface {
// Run starts name with args and working directory dir, waiting until exit.
Run(ctx context.Context, name string, args []string, dir string) error
}
// DefaultProcessRunner runs the process via os/exec with stdio inherited.
type DefaultProcessRunner struct{}
// Run implements ProcessRunner.
func (DefaultProcessRunner) Run(ctx context.Context, name string, args []string, dir string) error {
cmd := exec.CommandContext(ctx, name, args...)
cmd.Dir = dir
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("clientinject: run %s: %w", name, err)
}
return nil
}
// LaunchArgsFromPlan extracts launch CLI args from MutLaunchFlag mutations.
// Returns nil when the plan has no launch_flag mutation.
func LaunchArgsFromPlan(plan *Plan) []string {
if plan == nil {
return nil
}
for _, m := range plan.Mutations {
if m.Kind != MutLaunchFlag {
continue
}
switch d := m.Detail.(type) {
case LaunchFlagDetail:
return append([]string(nil), d.Args...)
case *LaunchFlagDetail:
if d != nil {
return append([]string(nil), d.Args...)
}
}
}
return nil
}
// IsPEMutationKind reports whether kind mutates a PE/DLL binary (not config/launch).
func IsPEMutationKind(k MutationKind) bool {
switch k {
case MutUSHeapString, MutLdstrRemap, MutRawOverwrite, MutPaddedString, MutAFVDisablePE:
return true
default:
return false
}
}

View File

@@ -0,0 +1,435 @@
package clientinject
import (
"context"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
)
// ShadowLaunchConfig controls hybrid Phase 1 shadow PE launch (Appendix B).
//
// Default hybrid algorithm:
// 1. Install root stays intact (DLLs/plugins stay put).
// 2. Temp dir via os.MkdirTemp("", "openfsd-client-shadow-*").
// 3. Copy only files that will be PE-mutated (typically vPilot.exe).
// 4. Apply PE mutations to temp copies.
// 5. Config: durable install rewrite by default (not temp).
// 6. Launch: exec temp PE with Dir = installRoot.
// 7. On exit: best-effort delete temp copies.
type ShadowLaunchConfig struct {
// DurableConfig applies config rewrites to the install root (default true).
// When false, config mutation targets are also copied into the temp tree
// (research escape hatch; not the Phase 1 default).
DurableConfig *bool
// KeepTemp skips temp-dir deletion after the process exits (debug/tests).
KeepTemp bool
// TempDir, when non-empty, is used instead of MkdirTemp (tests).
TempDir string
// Runner defaults to DefaultProcessRunner.
Runner ProcessRunner
// MkdirTemp overrides os.MkdirTemp (tests).
MkdirTemp func(dir, pattern string) (string, error)
// RemoveAll overrides os.RemoveAll (tests).
RemoveAll func(path string) error
}
func (c ShadowLaunchConfig) durableConfig() bool {
if c.DurableConfig == nil {
return true
}
return *c.DurableConfig
}
func (c ShadowLaunchConfig) runner() ProcessRunner {
if c.Runner != nil {
return c.Runner
}
return DefaultProcessRunner{}
}
func (c ShadowLaunchConfig) mkdirTemp(dir, pattern string) (string, error) {
if c.MkdirTemp != nil {
return c.MkdirTemp(dir, pattern)
}
return os.MkdirTemp(dir, pattern)
}
func (c ShadowLaunchConfig) removeAll(path string) error {
if c.RemoveAll != nil {
return c.RemoveAll(path)
}
return os.RemoveAll(path)
}
// ShadowSession describes a prepared hybrid shadow environment.
type ShadowSession struct {
// InstallRoot is the user install tree (CWD for the launched PE).
InstallRoot string
// TempDir holds shadowed PE copies (and optional non-durable config).
TempDir string
// TempPE is the absolute path of the shadowed primary PE.
TempPE string
// LaunchArgs are CLI flags for the client process.
LaunchArgs []string
// ShadowedFiles maps install absolute path → temp absolute path.
ShadowedFiles map[string]string
// StockPEChecksum is a cheap fingerprint of install PE bytes before launch
// (len + first 32 bytes hex) so tests can assert the install PE was left stock.
// Empty when no primary PE was shadowed.
InstallPEFingerprint string
keepTemp bool
removeAll func(path string) error
cleaned bool
}
// KeptTemp reports whether Cleanup intentionally left the temp directory
// (ShadowLaunchConfig.KeepTemp).
func (s *ShadowSession) KeptTemp() bool {
return s != nil && s.keepTemp
}
// Cleanup best-effort removes the temp shadow directory.
// Safe to call multiple times. No-op when KeepTemp was set.
func (s *ShadowSession) Cleanup() error {
if s == nil || s.cleaned || s.keepTemp || s.TempDir == "" {
return nil
}
s.cleaned = true
rm := s.removeAll
if rm == nil {
rm = os.RemoveAll
}
if err := rm(s.TempDir); err != nil {
return fmt.Errorf("clientinject: shadow cleanup %s: %w", s.TempDir, err)
}
return nil
}
// CollectShadowTargets returns unique absolute install paths that must be
// copied into the shadow temp dir (PE mutation targets only when durableConfig).
// Config paths are excluded when durableConfig is true.
func CollectShadowTargets(plan *Plan, durableConfig bool) []string {
if plan == nil {
return nil
}
seen := make(map[string]struct{})
var out []string
add := func(p string) {
if p == "" {
return
}
if !filepath.IsAbs(p) {
p = filepath.Join(plan.Install.RootDir, p)
}
p = filepath.Clean(p)
if _, ok := seen[p]; ok {
return
}
seen[p] = struct{}{}
out = append(out, p)
}
hasPEMut := false
for _, m := range plan.Mutations {
if !IsPEMutationKind(m.Kind) {
continue
}
hasPEMut = true
if m.TargetRel != "" {
add(m.TargetRel)
}
}
if hasPEMut {
add(AbsPrimaryPE(plan.Install))
}
if !durableConfig {
// Escape hatch: also shadow config rewrite targets.
for _, m := range plan.Mutations {
if m.Kind != MutConfigRewrite {
continue
}
if m.TargetRel != "" {
add(m.TargetRel)
}
switch d := m.Detail.(type) {
case ConfigRewriteDetail:
for _, p := range d.Paths {
add(p)
}
case *ConfigRewriteDetail:
if d != nil {
for _, p := range d.Paths {
add(p)
}
}
}
}
}
return out
}
// PrepareShadowLaunch creates the temp dir, copies shadow targets, and returns
// a session plus a plan whose PE mutations target the temp copies.
// Mutations are not applied yet.
//
// When durableConfig is true (default), config mutations still point at the
// install tree; only PE/DLL mutation targets are redirected.
func PrepareShadowLaunch(plan *Plan, w FileWriter, cfg ShadowLaunchConfig) (*ShadowSession, *Plan, error) {
if plan == nil {
return nil, nil, fmt.Errorf("clientinject: nil plan")
}
if len(plan.Blockers) > 0 {
return nil, nil, fmt.Errorf("clientinject: plan has blockers: %s", strings.Join(plan.Blockers, "; "))
}
if w == nil {
w = OSFileWriter{}
}
install := normalizeInstall(plan.Install)
if install.RootDir == "" {
return nil, nil, fmt.Errorf("clientinject: empty install root")
}
durable := cfg.durableConfig()
targets := CollectShadowTargets(plan, durable)
tempDir := cfg.TempDir
if tempDir == "" {
d, err := cfg.mkdirTemp("", "openfsd-client-shadow-*")
if err != nil {
return nil, nil, fmt.Errorf("clientinject: mkdir temp shadow: %w", err)
}
tempDir = d
} else {
if err := w.MkdirAll(tempDir, 0o700); err != nil {
return nil, nil, fmt.Errorf("clientinject: mkdir shadow dir %s: %w", tempDir, err)
}
}
session := &ShadowSession{
InstallRoot: install.RootDir,
TempDir: tempDir,
LaunchArgs: LaunchArgsFromPlan(plan),
ShadowedFiles: make(map[string]string),
keepTemp: cfg.KeepTemp,
removeAll: cfg.removeAll,
}
// Fingerprint install primary PE before any copy (stock proof).
pe := AbsPrimaryPE(install)
if pe != "" {
if data, err := w.ReadFile(pe); err == nil {
session.InstallPEFingerprint = peFingerprint(data)
}
}
// Copy each target into temp by basename (flat; no full-tree copy — KD-21).
// Collision on basename is refused.
baseSeen := make(map[string]string) // basename → install path
for _, src := range targets {
if _, err := w.Stat(src); err != nil {
// Missing targets skipped (Apply may create config when non-durable).
continue
}
base := filepath.Base(src)
if prev, ok := baseSeen[base]; ok && prev != src {
_ = session.Cleanup()
return nil, nil, fmt.Errorf("clientinject: shadow basename collision %q (%s vs %s)", base, prev, src)
}
baseSeen[base] = src
dst := filepath.Join(tempDir, base)
if err := w.CopyFile(src, dst); err != nil {
_ = session.Cleanup()
return nil, nil, fmt.Errorf("clientinject: shadow copy %s: %w", src, err)
}
session.ShadowedFiles[src] = dst
if pe != "" && filepath.Clean(src) == filepath.Clean(pe) {
session.TempPE = dst
}
}
// If no PE mutation targets were copied but we have a primary PE, still
// shadow the PE when the plan intends to launch a stock binary from temp
// (launch_flag-only plans). Prefer launching install PE with Dir=root in
// that case — TempPE stays empty and LaunchShadow uses install PE.
// (Design: copy only files that will be mutated.)
shadowPlan := clonePlanForShadow(plan, install, session, durable)
return session, shadowPlan, nil
}
// clonePlanForShadow redirects PE mutation targets to temp copies.
func clonePlanForShadow(plan *Plan, install Install, session *ShadowSession, durableConfig bool) *Plan {
out := &Plan{
Install: install,
Endpoints: plan.Endpoints,
Mutations: make([]Mutation, 0, len(plan.Mutations)),
Constraints: append([]Constraint(nil), plan.Constraints...),
Warnings: append([]string(nil), plan.Warnings...),
Blockers: append([]string(nil), plan.Blockers...),
}
if session.TempPE != "" {
out.Install.PrimaryPE = session.TempPE
}
for _, m := range plan.Mutations {
nm := m
switch {
case IsPEMutationKind(m.Kind):
// Redirect TargetRel to the temp absolute path when shadowed.
src := m.TargetRel
if src == "" {
src = AbsPrimaryPE(install)
} else if !filepath.IsAbs(src) {
src = filepath.Join(install.RootDir, src)
}
src = filepath.Clean(src)
if dst, ok := session.ShadowedFiles[src]; ok {
nm.TargetRel = dst
}
case m.Kind == MutConfigRewrite && !durableConfig:
// Rewrite config paths into the temp tree.
switch d := m.Detail.(type) {
case ConfigRewriteDetail:
nd := d
nd.Paths = remapPaths(d.Paths, session.ShadowedFiles, install.RootDir)
nm.Detail = nd
if m.TargetRel != "" {
nm.TargetRel = remapOne(m.TargetRel, session.ShadowedFiles, install.RootDir)
}
case *ConfigRewriteDetail:
if d != nil {
nd := *d
nd.Paths = remapPaths(d.Paths, session.ShadowedFiles, install.RootDir)
nm.Detail = &nd
}
if m.TargetRel != "" {
nm.TargetRel = remapOne(m.TargetRel, session.ShadowedFiles, install.RootDir)
}
}
}
out.Mutations = append(out.Mutations, nm)
}
return out
}
func remapPaths(paths []string, shadowed map[string]string, root string) []string {
if len(paths) == 0 {
return nil
}
out := make([]string, len(paths))
for i, p := range paths {
out[i] = remapOne(p, shadowed, root)
}
return out
}
func remapOne(p string, shadowed map[string]string, root string) string {
if p == "" {
return p
}
abs := p
if !filepath.IsAbs(abs) {
abs = filepath.Join(root, abs)
}
abs = filepath.Clean(abs)
if dst, ok := shadowed[abs]; ok {
return dst
}
// Not copied (missing source): place under temp by basename when possible.
// Callers that need creation should have copied or accept install path.
return p
}
func peFingerprint(data []byte) string {
n := len(data)
const head = 32
h := data
if len(h) > head {
h = h[:head]
}
return fmt.Sprintf("%d:%x", n, h)
}
// LaunchShadow prepares a hybrid shadow PE, applies mutations (PE→temp,
// config→install when durable), runs the process with Dir=installRoot, then
// best-effort cleans the temp dir.
//
// The install primary PE is left stock when only PE mutations were shadowed.
// Config is rewritten durably by default so CachedServers/status persist.
//
// launchArgs overrides plan launch_flag args when non-nil (empty slice clears).
func (e *Engine) LaunchShadow(ctx context.Context, plan *Plan, launchArgs []string, cfg ShadowLaunchConfig) (*ShadowSession, error) {
if plan == nil {
return nil, fmt.Errorf("clientinject: nil plan")
}
if len(plan.Blockers) > 0 {
return nil, fmt.Errorf("clientinject: plan has blockers: %s", strings.Join(plan.Blockers, "; "))
}
a, ok := e.Adapters[plan.Install.ClientID]
if !ok {
return nil, fmt.Errorf("clientinject: no adapter for client %q", plan.Install.ClientID)
}
w := e.writer()
session, shadowPlan, err := PrepareShadowLaunch(plan, w, cfg)
if err != nil {
return nil, err
}
// Apply mutations: PE to temp (via redirected PrimaryPE), config durable.
if err := a.Apply(ctx, shadowPlan, w); err != nil {
_ = session.Cleanup()
return nil, fmt.Errorf("clientinject: shadow apply: %w", err)
}
args := session.LaunchArgs
if launchArgs != nil {
args = append([]string(nil), launchArgs...)
session.LaunchArgs = args
}
exe := session.TempPE
if exe == "" {
// No PE mutations — launch stock install PE from install root.
exe = AbsPrimaryPE(plan.Install)
}
if exe == "" {
_ = session.Cleanup()
return nil, fmt.Errorf("clientinject: no PE to launch")
}
session.TempPE = exe // may be install PE when nothing was shadowed
runner := cfg.runner()
runErr := runner.Run(ctx, exe, args, session.InstallRoot)
// Always try cleanup after process exit (or run failure).
if cerr := session.Cleanup(); cerr != nil {
slog.Warn("clientinject shadow temp cleanup failed", "temp", session.TempDir, "err", cerr)
if runErr == nil {
runErr = cerr
}
}
if runErr != nil {
return session, runErr
}
return session, nil
}
// ShadowLaunch is a package-level convenience using the engine's writer/adapters.
// See Engine.LaunchShadow.
func ShadowLaunch(ctx context.Context, e *Engine, plan *Plan, launchArgs []string, cfg ShadowLaunchConfig) (*ShadowSession, error) {
if e == nil {
return nil, fmt.Errorf("clientinject: nil engine")
}
return e.LaunchShadow(ctx, plan, launchArgs, cfg)
}

View File

@@ -0,0 +1,452 @@
package clientinject
import (
"bytes"
"context"
"errors"
"os"
"path/filepath"
"strings"
"sync"
"testing"
)
// fakeRunner records the last process invocation without executing a PE.
type fakeRunner struct {
mu sync.Mutex
calls int
name string
args []string
dir string
runErr error
onRun func(name string, args []string, dir string)
}
func (f *fakeRunner) Run(_ context.Context, name string, args []string, dir string) error {
f.mu.Lock()
defer f.mu.Unlock()
f.calls++
f.name = name
f.args = append([]string(nil), args...)
f.dir = dir
if f.onRun != nil {
f.onRun(name, args, dir)
}
return f.runErr
}
func boolPtr(v bool) *bool { return &v }
func TestIsPEMutationKind(t *testing.T) {
if !IsPEMutationKind(MutUSHeapString) || !IsPEMutationKind(MutRawOverwrite) {
t.Fatal("expected PE kinds")
}
if IsPEMutationKind(MutConfigRewrite) || IsPEMutationKind(MutLaunchFlag) {
t.Fatal("config/launch must not be PE kinds")
}
}
func TestLaunchArgsFromPlan(t *testing.T) {
if LaunchArgsFromPlan(nil) != nil {
t.Fatal("nil plan")
}
plan := &Plan{Mutations: []Mutation{
{Kind: MutConfigRewrite},
{Kind: MutLaunchFlag, Detail: LaunchFlagDetail{Args: []string{"-novoice", "-x"}}},
}}
got := LaunchArgsFromPlan(plan)
if len(got) != 2 || got[0] != "-novoice" || got[1] != "-x" {
t.Fatalf("got %v", got)
}
}
func TestCollectShadowTargets_PEOnlyWhenDurable(t *testing.T) {
root := t.TempDir()
pe := filepath.Join(root, "app.exe")
cfg := filepath.Join(root, "config.xml")
plan := &Plan{
Install: Install{RootDir: root, PrimaryPE: pe, ConfigPaths: []string{cfg}},
Mutations: []Mutation{
{Kind: MutUSHeapString, TargetRel: "app.exe"},
{Kind: MutConfigRewrite, TargetRel: "config.xml", Detail: ConfigRewriteDetail{Paths: []string{cfg}}},
{Kind: MutLaunchFlag, Detail: LaunchFlagDetail{Args: []string{"-novoice"}}},
},
}
got := CollectShadowTargets(plan, true)
if len(got) != 1 || got[0] != pe {
t.Fatalf("durable shadow targets=%v want only PE", got)
}
gotAll := CollectShadowTargets(plan, false)
if len(gotAll) != 2 {
t.Fatalf("non-durable want PE+config, got %v", gotAll)
}
}
func TestLaunchShadow_PatchesTempLeavesInstallStock_CleansTemp(t *testing.T) {
root := t.TempDir()
pePath := filepath.Join(root, "app.exe")
payloadPath := filepath.Join(root, "payload.bin")
stockPE := []byte("MZ-STOCK-PE-BYTES-AAAAAAAA")
stockPayload := []byte("STOCK-PAYLOAD-CONTENT-XXXX")
if err := os.WriteFile(pePath, stockPE, 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(payloadPath, stockPayload, 0o644); err != nil {
t.Fatal(err)
}
// Side file that must never be copied (DLL / plugin stand-in).
dllPath := filepath.Join(root, "plugin.dll")
if err := os.WriteFile(dllPath, []byte("DLL-STOCK"), 0o644); err != nil {
t.Fatal(err)
}
patched := []byte("OPENFSD-PATCHED-PAYLOAD")
fa := &FakeAdapter{
ID: "fake",
Name: "Fake",
Profiles: []string{"fake-1"},
TargetRel: "payload.bin",
NewData: patched,
}
// Profile store not required for LaunchShadow (no Plan/Resolve).
eng := NewEngine(nil, fa)
plan := &Plan{
Install: Install{
ClientID: "fake",
RootDir: root,
PrimaryPE: pePath,
},
Mutations: []Mutation{
{
ID: "write_payload",
Kind: MutRawOverwrite,
TargetRel: "payload.bin",
Detail: WriteFileDetail{Contents: patched},
},
{
ID: "launch_flags",
Kind: MutLaunchFlag,
Detail: LaunchFlagDetail{Args: []string{"-serveraddressoverride", "h:6809", "-novoice"}},
},
},
}
var sawTempPayload []byte
runner := &fakeRunner{
onRun: func(name string, args []string, dir string) {
if dir != root {
t.Errorf("Dir=%q want install root %q", dir, root)
}
// name should be shadowed PE under temp, not install PE.
if name == pePath {
t.Errorf("launched install PE directly; want temp shadow")
}
if !strings.Contains(name, "openfsd-client-shadow-") && !strings.Contains(filepath.Dir(name), "shadow") {
// TempDir may be custom; at least must not be install root for PE.
if filepath.Dir(name) == root {
t.Errorf("temp PE still under install root: %s", name)
}
}
// Patched payload should live next to temp PE.
tempPayload := filepath.Join(filepath.Dir(name), "payload.bin")
b, err := os.ReadFile(tempPayload)
if err != nil {
t.Errorf("read temp payload: %v", err)
return
}
sawTempPayload = append([]byte(nil), b...)
// Install tree must still be stock while process "runs".
livePE, _ := os.ReadFile(pePath)
if !bytes.Equal(livePE, stockPE) {
t.Errorf("install PE mutated during launch")
}
livePayload, _ := os.ReadFile(payloadPath)
if !bytes.Equal(livePayload, stockPayload) {
t.Errorf("install payload mutated during launch: %q", livePayload)
}
// DLL must never have been copied into temp.
if _, err := os.Stat(filepath.Join(filepath.Dir(name), "plugin.dll")); err == nil {
t.Errorf("plugin.dll was shadow-copied; hybrid must not full-tree copy")
}
if len(args) < 2 || args[0] != "-serveraddressoverride" {
t.Errorf("args=%v", args)
}
},
}
// Track temp dir creation for cleanup assertion.
var createdTemp string
cfg := ShadowLaunchConfig{
Runner: runner,
MkdirTemp: func(dir, pattern string) (string, error) {
d, err := os.MkdirTemp(dir, pattern)
createdTemp = d
return d, err
},
}
session, err := eng.LaunchShadow(context.Background(), plan, nil, cfg)
if err != nil {
t.Fatalf("LaunchShadow: %v", err)
}
if runner.calls != 1 {
t.Fatalf("runner calls=%d", runner.calls)
}
if session == nil {
t.Fatal("nil session")
}
if session.InstallPEFingerprint == "" {
t.Fatal("expected install PE fingerprint")
}
// Install PE still stock after exit.
livePE, err := os.ReadFile(pePath)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(livePE, stockPE) {
t.Fatalf("install PE not stock after shadow launch: %q", livePE)
}
livePayload, _ := os.ReadFile(payloadPath)
if !bytes.Equal(livePayload, stockPayload) {
t.Fatalf("install payload not stock: %q", livePayload)
}
if !bytes.Equal(sawTempPayload, patched) {
t.Fatalf("temp payload not patched: %q", sawTempPayload)
}
// Temp dir best-effort deleted.
if createdTemp != "" {
if _, err := os.Stat(createdTemp); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("temp dir not cleaned: %s err=%v", createdTemp, err)
}
}
// DLL untouched.
dll, _ := os.ReadFile(dllPath)
if !bytes.Equal(dll, []byte("DLL-STOCK")) {
t.Fatal("dll mutated")
}
}
func TestLaunchShadow_DurableConfigRewritesInstall(t *testing.T) {
root := t.TempDir()
pePath := filepath.Join(root, "app.exe")
cfgPath := filepath.Join(root, "app.config")
stockPE := []byte("MZ-STOCK")
if err := os.WriteFile(pePath, stockPE, 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(cfgPath, []byte("OLD-CONFIG"), 0o644); err != nil {
t.Fatal(err)
}
// Adapter that writes config via WriteFileDetail and PE-ish file via TargetRel.
fa := &configAndPEAdapter{peRel: "app.exe", cfgPath: cfgPath, pePatch: []byte("SHADOW-PE"), cfgPatch: []byte("NEW-CONFIG")}
eng := NewEngine(nil, fa)
plan := &Plan{
Install: Install{ClientID: "cfgpe", RootDir: root, PrimaryPE: pePath},
Mutations: []Mutation{
{
ID: "pe",
Kind: MutRawOverwrite,
TargetRel: "app.exe",
Detail: WriteFileDetail{Contents: []byte("SHADOW-PE")},
},
{
ID: "cfg",
Kind: MutConfigRewrite,
TargetRel: "app.config",
Detail: WriteFileDetail{ // Fake path: adapter below handles MutConfigRewrite
Contents: []byte("NEW-CONFIG"),
},
},
},
}
runner := &fakeRunner{}
session, err := eng.LaunchShadow(context.Background(), plan, []string{"-novoice"}, ShadowLaunchConfig{
Runner: runner,
DurableConfig: boolPtr(true),
})
if err != nil {
t.Fatalf("LaunchShadow: %v", err)
}
_ = session
// Install PE stock.
gotPE, _ := os.ReadFile(pePath)
if !bytes.Equal(gotPE, stockPE) {
t.Fatalf("install PE mutated: %q", gotPE)
}
// Durable config applied to install.
gotCfg, _ := os.ReadFile(cfgPath)
if !bytes.Equal(gotCfg, []byte("NEW-CONFIG")) {
t.Fatalf("config not durably rewritten: %q", gotCfg)
}
if runner.dir != root {
t.Fatalf("cwd=%q", runner.dir)
}
if runner.name == pePath {
t.Fatal("should launch temp PE, not install PE")
}
}
// configAndPEAdapter applies WriteFileDetail for PE kinds to TargetRel and for
// MutConfigRewrite to the absolute cfg path (durable).
type configAndPEAdapter struct {
peRel, cfgPath string
pePatch, cfgPatch []byte
}
func (a *configAndPEAdapter) ClientID() string { return "cfgpe" }
func (a *configAndPEAdapter) DisplayName() string { return "cfgpe" }
func (a *configAndPEAdapter) SupportedProfiles() []string { return nil }
func (a *configAndPEAdapter) Discover(context.Context) ([]InstallCandidate, error) {
return nil, nil
}
func (a *configAndPEAdapter) Verify(Install, *Profile) error { return nil }
func (a *configAndPEAdapter) EndpointConstraints(*Profile) []Constraint { return nil }
func (a *configAndPEAdapter) Plan(Install, *Profile, Endpoints) (*Plan, error) {
return nil, errors.New("unused")
}
func (a *configAndPEAdapter) HealthCheck(Install, Endpoints) error { return nil }
func (a *configAndPEAdapter) LaunchArgs(Install, Endpoints) []string {
return []string{"-novoice"}
}
func (a *configAndPEAdapter) Apply(_ context.Context, plan *Plan, w FileWriter) error {
for _, m := range plan.Mutations {
switch m.Kind {
case MutRawOverwrite, MutUSHeapString, MutPaddedString, MutAFVDisablePE, MutLdstrRemap:
path := m.TargetRel
if path == "" {
path = AbsPrimaryPE(plan.Install)
} else if !filepath.IsAbs(path) {
path = filepath.Join(plan.Install.RootDir, path)
}
d, ok := m.Detail.(WriteFileDetail)
if !ok {
if p, ok2 := m.Detail.(*WriteFileDetail); ok2 && p != nil {
d = *p
} else {
return errors.New("bad PE detail")
}
}
if err := w.WriteFile(path, d.Contents); err != nil {
return err
}
case MutConfigRewrite:
// Always write durable path a.cfgPath (simulates absolute config paths).
d, ok := m.Detail.(WriteFileDetail)
if !ok {
if p, ok2 := m.Detail.(*WriteFileDetail); ok2 && p != nil {
d = *p
} else {
d = WriteFileDetail{Contents: a.cfgPatch}
}
}
if err := w.WriteFile(a.cfgPath, d.Contents); err != nil {
return err
}
case MutLaunchFlag:
continue
default:
return errors.New("unsupported")
}
}
return nil
}
func TestPrepareShadowLaunch_Blockers(t *testing.T) {
_, _, err := PrepareShadowLaunch(&Plan{Blockers: []string{"x"}}, OSFileWriter{}, ShadowLaunchConfig{})
if err == nil {
t.Fatal("expected blockers error")
}
}
func TestLaunchShadow_CleanupOnApplyFailure(t *testing.T) {
root := t.TempDir()
pePath := filepath.Join(root, "app.exe")
if err := os.WriteFile(pePath, []byte("MZ"), 0o644); err != nil {
t.Fatal(err)
}
fa := &FakeAdapter{ID: "fake", Name: "F", FailApply: true, TargetRel: "payload.bin"}
// Need payload for shadow copy? Fake apply fails before write — still prepare copies PE.
if err := os.WriteFile(filepath.Join(root, "payload.bin"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
eng := NewEngine(nil, fa)
plan := &Plan{
Install: Install{ClientID: "fake", RootDir: root, PrimaryPE: pePath},
Mutations: []Mutation{{
ID: "w", Kind: MutRawOverwrite, TargetRel: "payload.bin",
Detail: WriteFileDetail{Contents: []byte("y")},
}},
}
var created string
_, err := eng.LaunchShadow(context.Background(), plan, nil, ShadowLaunchConfig{
Runner: &fakeRunner{},
MkdirTemp: func(dir, pattern string) (string, error) {
d, err := os.MkdirTemp(dir, pattern)
created = d
return d, err
},
})
if err == nil {
t.Fatal("expected apply failure")
}
if created != "" {
if _, st := os.Stat(created); !errors.Is(st, os.ErrNotExist) {
t.Fatalf("temp not cleaned after apply fail: %v", st)
}
}
}
func TestLaunchShadow_KeepTemp(t *testing.T) {
root := t.TempDir()
pePath := filepath.Join(root, "app.exe")
payload := filepath.Join(root, "payload.bin")
_ = os.WriteFile(pePath, []byte("MZ"), 0o644)
_ = os.WriteFile(payload, []byte("stock"), 0o644)
fa := &FakeAdapter{ID: "fake", Name: "F", TargetRel: "payload.bin", NewData: []byte("new")}
eng := NewEngine(nil, fa)
plan := &Plan{
Install: Install{ClientID: "fake", RootDir: root, PrimaryPE: pePath},
Mutations: []Mutation{{
ID: "w", Kind: MutRawOverwrite, TargetRel: "payload.bin",
Detail: WriteFileDetail{Contents: []byte("new")},
}},
}
var created string
session, err := eng.LaunchShadow(context.Background(), plan, nil, ShadowLaunchConfig{
KeepTemp: true,
Runner: &fakeRunner{},
MkdirTemp: func(dir, pattern string) (string, error) {
d, err := os.MkdirTemp(dir, pattern)
created = d
return d, err
},
})
if err != nil {
t.Fatal(err)
}
if _, err := os.Stat(created); err != nil {
t.Fatalf("KeepTemp should leave dir: %v", err)
}
// Manual cleanup for test hygiene.
if err := session.Cleanup(); err != nil {
// Cleanup is no-op when keepTemp — remove manually.
_ = os.RemoveAll(created)
} else {
// keepTemp means Cleanup is no-op
_ = os.RemoveAll(created)
}
}
func TestShadowLaunch_NilEngine(t *testing.T) {
_, err := ShadowLaunch(context.Background(), nil, &Plan{}, nil, ShadowLaunchConfig{})
if err == nil {
t.Fatal("expected error")
}
}