Skip to content

feat: VmsanContext composition root with hooks, plugins, and logger#23

Merged
angelorc merged 9 commits intomainfrom
feat/vmsan-context
Mar 2, 2026
Merged

feat: VmsanContext composition root with hooks, plugins, and logger#23
angelorc merged 9 commits intomainfrom
feat/vmsan-context

Conversation

@angelorc
Copy link
Copy Markdown
Owner

@angelorc angelorc commented Mar 1, 2026

Summary

  • Add VmsanContext composition root (createVmsan()) with injectable paths, store, hooks, logger, and plugins
  • Add lifecycle hooks system via hookable (12 events: vm:beforeCreate, vm:afterCreate, vm:beforeStart, vm:afterStart, vm:beforeStop, vm:afterStop, vm:beforeRemove, vm:afterRemove, vm:error, network:afterSetup, network:afterTeardown, network:policyChange)
  • Add VmsanPlugin interface with definePlugin() helper
  • Add VmsanLogger abstraction with createDefaultLogger() (consola) and createSilentLogger()
  • Add MemoryVmStateStore for testing/embedding
  • Extract getActiveTapSlots() as public function
  • Move create() and start() orchestration from CLI commands into VMService
  • Simplify all CLI commands to use const vmsan = await createVmsan()
  • Exclude *.md from oxfmt in lint/fmt scripts

Test plan

  • bun run typecheck passes
  • bun run build passes
  • bun run lint passes (only pre-existing warnings)
  • Manual: sudo vmsan create --vcpus 1 --mem 128 works identically
  • Manual: sudo vmsan start/stop/remove lifecycle works
  • Verify programmatic usage: import { createVmsan } from "vmsan" and call .create()

Summary by CodeRabbit

  • New Features

    • Plugin system and lifecycle hooks for VM events
    • Context-based VM manager with expanded public VM service API
    • In-memory VM state store option and improved logger with tag scoping
    • Utility to normalize errors
  • Refactor

    • Commands and VM workflows migrated to the shared runtime context
    • Network slot/state handling reorganized and exposed for reuse
  • Chores

    • Lint/format scripts updated; added hookable dependency
  • Style

    • Standardized string quoting and minor config/CSS formatting fixes

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vmsan Ready Ready Preview, Comment Mar 2, 2026 0:01am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 1, 2026

📝 Walkthrough

Walkthrough

Adds a Vmsan runtime context and factory (createVmsan), a plugin/hook system, typed logger utilities, an in-memory state store, and refactors VMService and CLI commands to use the new context. Also includes minor docs/style and package.json script/dependency edits.

Changes

Cohort / File(s) Summary
Configuration & Styling
docs/app/app.config.ts, docs/app/assets/css/main.css, docs/nuxt.config.ts
Normalized string quoting and minor formatting; no functional changes.
Package & Scripts
package.json
Added dependency hookable (^6.0.1); updated lint/format scripts to exclude Markdown/docs.
Context & Factory
src/context.ts, src/index.ts
Added VmsanOptions, VmsanContext, and createVmsan() factory; re-exported new context, hooks, plugin, and logger types from public index.
Hooks & Plugins
src/hooks.ts, src/plugin.ts
Introduced VmsanHooks interface and VmsanPlugin + definePlugin API for plugin setup.
Logger
src/vmsan-logger.ts
Added VmsanLogger interface and factories createDefaultLogger / createSilentLogger.
VM Service Refactor
src/services/vm.ts, src/lib/vm-state.ts
Refactored VMService to accept VmsanContext and expose lifecycle methods (create, start, stop, remove, updateNetworkPolicy, get) and new result/option types; moved/ exported network-slot helpers (getActiveTapSlots, findFreeNetworkSlot) to module scope.
State Stores
src/stores/memory.ts
Added MemoryVmStateStore implementing VmStateStore, including allocateNetworkSlot() delegating to findFreeNetworkSlot.
CLI Commands
src/commands/...
src/commands/create.ts, src/commands/list.ts, src/commands/network.ts, src/commands/remove.ts, src/commands/start.ts, src/commands/stop.ts
Commands now use await createVmsan() and call service methods on the returned instance; many low-level lifecycle/boot/network orchestration details removed from command implementations and centralized in VMService.
Utilities
src/lib/utils.ts
Added toError(err: unknown): Error utility to normalize errors for internal use.
Public API surface
src/index.ts
Expanded exports to include createVmsan, context/types, hooks, plugin helpers, logger factories, MemoryVmStateStore, and various VMService types and helpers.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as CLI (commands)
  participant Factory as createVmsan()
  participant VM as VMService
  participant Store as VmStateStore
  participant Hooks as VmsanHooks
  participant Logger as VmsanLogger

  CLI->>Factory: await createVmsan(options)
  Factory->>Store: instantiate or resolve store
  Factory->>Logger: create or resolve logger
  Factory->>Hooks: create hookable instance
  Factory-->>CLI: return VMService (ctx wired)

  CLI->>VM: VMService.create(CreateVmOptions)
  VM->>Hooks: emit "vm:beforeCreate"
  VM->>Store: allocateNetworkSlot / save state
  VM->>Logger: info/debug about creation steps
  VM->>Hooks: emit "network:afterSetup"
  VM->>Store: update state (running/started)
  VM->>Hooks: emit "vm:afterCreate"
  VM-->>CLI: CreateVmResult (vmId, pid, state)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 I hopped in with a factory new,
Hooks and plugins stitched into view,
A logger hums, a store holds fast,
Commands now call the context vast,
Hop, build, and run — the changes through!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: introducing VmsanContext as a composition root with hooks, plugins, and logger functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/vmsan-context

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
src/services/vm.ts (1)

721-727: Tighten policy type at the service boundary

Line 723 accepts policy: string; this weakens compile-time validation for policy values.

✅ Small type-safety improvement
  async updateNetworkPolicy(
    vmId: string,
-    policy: string,
+    policy: NetworkPolicy,
    domains: string[],
    allowedCidrs: string[],
    deniedCidrs: string[],
  ): Promise<UpdatePolicyResult> {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/vm.ts` around lines 721 - 727, The updateNetworkPolicy signature
currently accepts policy: string which weakens type safety; change it to a
stricter type (e.g., a Policy enum or union type such as Policy or VMPolicy) and
update the function signature of updateNetworkPolicy(vmId: string, policy:
Policy, ...). Import or define the policy type next to existing VM types (or
reuse an existing Policy/VMPolicy type) and update any callers to pass that
typed value so TypeScript enforces allowed policy values at the service
boundary; keep the return type UpdatePolicyResult unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/commands/list.ts`:
- Around line 27-31: Wrap the createVmsan() call inside async run() with a
try/catch so initialization failures are handled locally: call createVmsan() in
a try block and in the catch use the command logger (cmdLog) to record a
descriptive error including the caught error object, then exit the run early
(return or propagate a controlled error) instead of letting the rejection bypass
CLI error/reporting; locate this change around the async run function where
createVmsan() and vmsan.list() are used.

In `@src/services/vm.ts`:
- Around line 712-714: The catch blocks that currently return { vmId, success:
false, error: err instanceof VmsanError ? err : undefined } (around the vm
stop/remove code) must also emit the lifecycle error event before returning so
plugins receive failure notifications; locate the catch in the stop/remove
functions (references: vmId, VmsanError) and call the same event emitter used by
create/start (emit "vm:error" with the vmId and error payload) immediately prior
to the return, ensuring the emitted error mirrors what create/start emit for
consistency (also apply the same change to the other catch at the 844–846 area).
- Around line 304-325: The restart path must honor the same create-time
isolation flags: update the restart logic that calls jailer.spawn (where
newPidNs is currently hardcoded true and cgroup/seccomp are hardcoded) to reuse
the same computations as the creation path (reuse seccompFilter from
ensureSeccompFilter or undefined when opts.disableSeccomp, compute cgroup
exactly like the creation block using opts.disableCgroup and
vcpus/memMib/CGROUP_VMM_OVERHEAD_MIB, and set newPidNs to !opts.disablePidNs).
Refactor the duplicated spawn preparation into a small helper or shared
variables (seccompFilter, cgroup, newPidNs) so both the create path and the
restart path call jailer.spawn with identical semantics.
- Around line 461-498: The cleanup code building vmRootCandidates from
state.chrootDir and dirname(dirname(state.apiSocket)) (used in vmRootCandidates,
removeStaleDevTrees, removeStaleDeviceNodes and the unlinkSync(rmSync) calls)
must validate these resolved paths are inside an expected managed root before
performing destructive ops; implement checks to reject empty, root ("/") or
paths that escape the managed root by resolving real paths (realpathSync) and
ensuring each candidate startsWith or isPathInside the configured managedRoot
(or whitelist) and only then call unlinkSync/rmSync, otherwise skip and log a
warning; also validate state.apiSocket/state.chrootDir are non-null and
canonicalize them before use to prevent directory traversal/tampering.
- Line 731: The code currently calls fileLock.run with an async callback, which
releases the lock immediately because run is synchronous; replace that call with
fileLock.runAsync so the lock is held until the async callback finishes—i.e.,
locate the call to fileLock.run(...) and change it to fileLock.runAsync(...)
ensuring the async function passed remains unchanged so the lock semantics are
correct.

---

Nitpick comments:
In `@src/services/vm.ts`:
- Around line 721-727: The updateNetworkPolicy signature currently accepts
policy: string which weakens type safety; change it to a stricter type (e.g., a
Policy enum or union type such as Policy or VMPolicy) and update the function
signature of updateNetworkPolicy(vmId: string, policy: Policy, ...). Import or
define the policy type next to existing VM types (or reuse an existing
Policy/VMPolicy type) and update any callers to pass that typed value so
TypeScript enforces allowed policy values at the service boundary; keep the
return type UpdatePolicyResult unchanged.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0c5e946 and 7eba13a.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • docs/app/app.config.ts
  • docs/app/assets/css/main.css
  • docs/nuxt.config.ts
  • package.json
  • src/commands/create.ts
  • src/commands/list.ts
  • src/commands/network.ts
  • src/commands/remove.ts
  • src/commands/start.ts
  • src/commands/stop.ts
  • src/context.ts
  • src/hooks.ts
  • src/index.ts
  • src/lib/vm-state.ts
  • src/plugin.ts
  • src/services/vm.ts
  • src/stores/memory.ts
  • src/vmsan-logger.ts

Comment thread src/commands/list.ts Outdated
Comment thread src/services/vm.ts
Comment on lines +304 to +325
const seccompFilter = opts.disableSeccomp ? undefined : ensureSeccompFilter(paths);
if (seccompFilter) {
logger.debug(`Seccomp filter: ${seccompFilter}`);
}

const cgroup: CgroupConfig | undefined = opts.disableCgroup
? undefined
: {
cpuQuotaUs: vcpus * 100000,
cpuPeriodUs: 100000,
memoryBytes: (memMib + CGROUP_VMM_OVERHEAD_MIB) * 1024 * 1024,
};

jailer.spawn({
firecrackerBin,
jailerBin,
chrootBase: jailerPaths.chrootBase,
seccompFilter: seccompFilter ?? undefined,
newPidNs: !opts.disablePidNs,
cgroup,
netns: netnsName,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Restart path ignores create-time isolation flags

Line 304 applies disableSeccomp/disablePidNs/disableCgroup during create, but Line 554 and Line 539 hardcode restart behavior (newPidNs: true, always-on cgroup, no seccomp filter). This can change runtime guarantees and break restart on hosts needing disabled cgroups/namespaces.

♻️ Proposed direction
// Persist these flags in VmState at create-time (requires VmState/buildInitialVmState updates)
+disableSeccomp: !!opts.disableSeccomp,
+disablePidNs: !!opts.disablePidNs,
+disableCgroup: !!opts.disableCgroup,

// Reuse persisted flags on start
-const cgroup: CgroupConfig = {
+const cgroup: CgroupConfig | undefined = state.disableCgroup
+  ? undefined
+  : {
     cpuQuotaUs: state.vcpuCount * 100000,
     cpuPeriodUs: 100000,
     memoryBytes: (state.memSizeMib + CGROUP_VMM_OVERHEAD_MIB) * 1024 * 1024,
-};
+  };

+const seccompFilter = state.disableSeccomp ? undefined : ensureSeccompFilter(paths);

jailer.spawn({
  firecrackerBin,
  jailerBin,
  chrootBase: jailer.paths.chrootBase,
- newPidNs: true,
+ seccompFilter,
+ newPidNs: !state.disablePidNs,
  cgroup,
  netns: state.network.netnsName,
});

Also applies to: 539-557

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/vm.ts` around lines 304 - 325, The restart path must honor the
same create-time isolation flags: update the restart logic that calls
jailer.spawn (where newPidNs is currently hardcoded true and cgroup/seccomp are
hardcoded) to reuse the same computations as the creation path (reuse
seccompFilter from ensureSeccompFilter or undefined when opts.disableSeccomp,
compute cgroup exactly like the creation block using opts.disableCgroup and
vcpus/memMib/CGROUP_VMM_OVERHEAD_MIB, and set newPidNs to !opts.disablePidNs).
Refactor the duplicated spawn preparation into a small helper or shared
variables (seccompFilter, cgroup, newPidNs) so both the create path and the
restart path call jailer.spawn with identical semantics.

Comment thread src/services/vm.ts
Comment on lines +461 to +498
const vmRootCandidates = Array.from(
new Set([
join(state.chrootDir, "root"),
state.chrootDir,
dirname(dirname(state.apiSocket)),
]),
);
for (const rootDir of vmRootCandidates) {
const staleFirecrackerBin = join(rootDir, "firecracker");
if (existsSync(staleFirecrackerBin)) {
unlinkSync(staleFirecrackerBin);
}
rmSync(join(rootDir, "firecracker.pid"), { force: true });
}

const socketPath = state.apiSocket;
if (existsSync(socketPath)) {
unlinkSync(socketPath);
}

const removeStaleDevTrees = (): void => {
for (const rootDir of vmRootCandidates) {
const devDir = join(rootDir, "dev");
if (existsSync(devDir)) {
rmSync(devDir, { recursive: true, force: true });
}
}
};
const removeStaleDeviceNodes = (): void => {
const staleNodes = ["dev/net/tun", "dev/kvm", "dev/userfaultfd", "dev/urandom"];
for (const rootDir of vmRootCandidates) {
for (const rel of staleNodes) {
const nodePath = join(rootDir, rel);
if (existsSync(nodePath)) {
rmSync(nodePath, { recursive: true, force: true });
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard destructive file operations with managed-path validation

Line 461 and Line 823 derive deletion targets from persisted state (state.chrootDir, state.apiSocket) and then call unlinkSync/rmSync. If state is tampered/corrupted, this can target arbitrary host paths.

🛡️ Proposed fix
-import { dirname, join } from "node:path";
+import { dirname, isAbsolute, join, relative, resolve } from "node:path";

 export class VMService {
+  private assertManagedPath(targetPath: string): string {
+    const base = resolve(this.paths.jailerBaseDir);
+    const target = resolve(targetPath);
+    const rel = relative(base, target);
+    if (rel.startsWith("..") || isAbsolute(rel)) {
+      throw new VmsanError(`Refusing to operate on unmanaged path: ${targetPath}`);
+    }
+    return target;
+  }

   async start(vmId: string): Promise<StartVmResult> {
@@
-      const vmRootCandidates = Array.from(
+      const safeChrootDir = this.assertManagedPath(state.chrootDir);
+      const safeSocketPath = this.assertManagedPath(state.apiSocket);
+      const vmRootCandidates = Array.from(
         new Set([
-          join(state.chrootDir, "root"),
-          state.chrootDir,
-          dirname(dirname(state.apiSocket)),
+          join(safeChrootDir, "root"),
+          safeChrootDir,
+          dirname(dirname(safeSocketPath)),
         ]),
       );
@@
-      const socketPath = state.apiSocket;
+      const socketPath = safeSocketPath;
@@
   async remove(vmId: string, opts?: { force?: boolean }): Promise<StopResult> {
@@
       if (state.chrootDir) {
-        const vmJailerDir = dirname(state.chrootDir);
+        const safeChrootDir = this.assertManagedPath(state.chrootDir);
+        const vmJailerDir = dirname(safeChrootDir);
         try {
-          rmSync(state.chrootDir, { recursive: true, force: true });
+          rmSync(safeChrootDir, { recursive: true, force: true });
         } catch {

Also applies to: 823-832

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/vm.ts` around lines 461 - 498, The cleanup code building
vmRootCandidates from state.chrootDir and dirname(dirname(state.apiSocket))
(used in vmRootCandidates, removeStaleDevTrees, removeStaleDeviceNodes and the
unlinkSync(rmSync) calls) must validate these resolved paths are inside an
expected managed root before performing destructive ops; implement checks to
reject empty, root ("/") or paths that escape the managed root by resolving real
paths (realpathSync) and ensuring each candidate startsWith or isPathInside the
configured managedRoot (or whitelist) and only then call unlinkSync/rmSync,
otherwise skip and log a warning; also validate state.apiSocket/state.chrootDir
are non-null and canonicalize them before use to prevent directory
traversal/tampering.

Comment thread src/services/vm.ts
Comment thread src/services/vm.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
src/services/vm.ts (2)

446-464: ⚠️ Potential issue | 🔴 Critical

Guard destructive filesystem cleanup with managed-path validation.

Line 446–464 and Line 807–816 build deletion targets from persisted state and call unlinkSync/rmSync directly. If state.chrootDir/state.apiSocket is corrupted or tampered, cleanup can delete arbitrary host paths.

🛡️ Proposed fix
-import { dirname, join } from "node:path";
+import { dirname, isAbsolute, join, relative, resolve } from "node:path";

 export class VMService {
+  private assertManagedPath(targetPath: string): string {
+    const base = resolve(this.paths.jailerBaseDir);
+    const target = resolve(targetPath);
+    const rel = relative(base, target);
+    if (!targetPath || target === "/" || rel.startsWith("..") || isAbsolute(rel)) {
+      throw new VmsanError(`Refusing to operate on unmanaged path: ${targetPath}`);
+    }
+    return target;
+  }

   async start(vmId: string): Promise<StartVmResult> {
@@
-    const vmRootCandidates = Array.from(
+    const safeChrootDir = this.assertManagedPath(state.chrootDir);
+    const safeSocketPath = this.assertManagedPath(state.apiSocket);
+    const vmRootCandidates = Array.from(
       new Set([
-        join(state.chrootDir, "root"),
-        state.chrootDir,
-        dirname(dirname(state.apiSocket)),
+        join(safeChrootDir, "root"),
+        safeChrootDir,
+        dirname(dirname(safeSocketPath)),
       ]),
     );
@@
-    const socketPath = state.apiSocket;
+    const socketPath = safeSocketPath;

   async remove(vmId: string, opts?: { force?: boolean }): Promise<StopResult> {
@@
-      const vmJailerDir = dirname(state.chrootDir);
+      const safeChrootDir = this.assertManagedPath(state.chrootDir);
+      const vmJailerDir = dirname(safeChrootDir);
       try {
-        rmSync(state.chrootDir, { recursive: true, force: true });
+        rmSync(safeChrootDir, { recursive: true, force: true });
       } catch {}

Also applies to: 466-483, 807-816

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/vm.ts` around lines 446 - 464, The cleanup code that unlinks
files (symbols: vmRootCandidates, staleFirecrackerBin, socketPath) uses
persisted state (state.chrootDir, state.apiSocket) directly and can delete
arbitrary host paths if those values are corrupted; before calling
unlinkSync/rmSync, resolve each candidate with realpath/resolve and verify it is
inside the managed VM root (e.g. ensure
realpath(candidate).startsWith(realpath(managedRoot))) and reject/skip any path
that fails validation (also handle symlink escapes by using fs.realpathSync).
Apply the same validation for socketPath and log/throw and skip deletion when a
target is outside the expected chroot/api directory to prevent destructive
cleanup.

293-313: ⚠️ Potential issue | 🟠 Major

Restart path still diverges from create-time isolation flags.

Line 293–313 honors disableSeccomp/disablePidNs/disableCgroup, but Line 524–542 restarts with newPidNs: true, always-on cgroup, and no seccomp filter. This can change runtime guarantees and cause restart failures on hosts that required those create-time flags.

♻️ Proposed fix
// During create: persist effective isolation flags in VM state
 this.store.save(buildInitialVmState({
   ...
+  disableSeccomp: !!opts.disableSeccomp,
+  disablePidNs: !!opts.disablePidNs,
+  disableCgroup: !!opts.disableCgroup,
 }));

// During start: reuse persisted flags
-const cgroup: CgroupConfig = {
-  cpuQuotaUs: state.vcpuCount * 100000,
-  cpuPeriodUs: 100000,
-  memoryBytes: (state.memSizeMib + CGROUP_VMM_OVERHEAD_MIB) * 1024 * 1024,
-};
+const seccompFilter = state.disableSeccomp ? undefined : ensureSeccompFilter(paths);
+const cgroup: CgroupConfig | undefined = state.disableCgroup
+  ? undefined
+  : {
+      cpuQuotaUs: state.vcpuCount * 100000,
+      cpuPeriodUs: 100000,
+      memoryBytes: (state.memSizeMib + CGROUP_VMM_OVERHEAD_MIB) * 1024 * 1024,
+    };

 jailer.spawn({
   firecrackerBin,
   jailerBin,
   chrootBase: jailer.paths.chrootBase,
-  newPidNs: true,
+  seccompFilter,
+  newPidNs: !state.disablePidNs,
   cgroup,
   netns: state.network.netnsName,
 });

Also applies to: 524-542

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/vm.ts` around lines 293 - 313, The restart path diverges from
the create-time isolation flags: replicate the same logic that computes
seccompFilter (ensureSeccompFilter/opts.disableSeccomp), cgroup
(opts.disableCgroup and computed cpu/memory values), and newPidNs
(!opts.disablePidNs) for the restart's jailer.spawn call instead of hardcoding
newPidNs: true / always-on cgroup / no seccomp; either reuse the seccompFilter
and cgroup variables defined around seccompFilter/cgroup or extract that
computation into a shared helper (e.g., computeIsolationConfig) and call it from
both the create and restart code paths so jailer.spawn uses identical
seccompFilter, cgroup, and newPidNs settings on restart.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/services/vm.ts`:
- Around line 446-464: The cleanup code that unlinks files (symbols:
vmRootCandidates, staleFirecrackerBin, socketPath) uses persisted state
(state.chrootDir, state.apiSocket) directly and can delete arbitrary host paths
if those values are corrupted; before calling unlinkSync/rmSync, resolve each
candidate with realpath/resolve and verify it is inside the managed VM root
(e.g. ensure realpath(candidate).startsWith(realpath(managedRoot))) and
reject/skip any path that fails validation (also handle symlink escapes by using
fs.realpathSync). Apply the same validation for socketPath and log/throw and
skip deletion when a target is outside the expected chroot/api directory to
prevent destructive cleanup.
- Around line 293-313: The restart path diverges from the create-time isolation
flags: replicate the same logic that computes seccompFilter
(ensureSeccompFilter/opts.disableSeccomp), cgroup (opts.disableCgroup and
computed cpu/memory values), and newPidNs (!opts.disablePidNs) for the restart's
jailer.spawn call instead of hardcoding newPidNs: true / always-on cgroup / no
seccomp; either reuse the seccompFilter and cgroup variables defined around
seccompFilter/cgroup or extract that computation into a shared helper (e.g.,
computeIsolationConfig) and call it from both the create and restart code paths
so jailer.spawn uses identical seccompFilter, cgroup, and newPidNs settings on
restart.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7eba13a and 7bf5582.

📒 Files selected for processing (3)
  • package.json
  • src/commands/list.ts
  • src/services/vm.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/services/vm.ts (2)

427-439: ⚠️ Potential issue | 🔴 Critical

Validate managed paths before destructive cleanup operations.

Line 427-Line 456 and Line 762 use persisted state.chrootDir / state.apiSocket to drive rmSync/cleanup without managed-root validation. If state is tampered, this can target arbitrary host paths.

🛡️ Suggested direction
+import { isAbsolute, relative, resolve } from "node:path";
+
+private assertManagedPath(targetPath: string): string {
+  const base = resolve(this.paths.jailerBaseDir);
+  const target = resolve(targetPath);
+  const rel = relative(base, target);
+  if (!targetPath || target === "/" || rel.startsWith("..") || isAbsolute(rel)) {
+    throw new VmsanError(`Refusing to operate on unmanaged path: ${targetPath}`);
+  }
+  return target;
+}

Use assertManagedPath(...) for state.chrootDir, state.apiSocket, and all derived cleanup candidates before rmSync/cleanupChroot.

Also applies to: 443-456, 762-763

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/vm.ts` around lines 427 - 439, The cleanup uses persisted state
paths (state.chrootDir, state.apiSocket) to build vmRootCandidates and then
calls rmSync in removeStaleFirecrackerFiles (and calls cleanupChroot elsewhere)
without validating them; call assertManagedPath(...) on state.chrootDir,
state.apiSocket and on each derived path in vmRootCandidates before performing
any rmSync or cleanupChroot operations to ensure they are within the managed
root, and bail/throw if validation fails so no rmSync runs on
unvalidated/tainted paths.

495-509: ⚠️ Potential issue | 🟠 Major

Restart path still diverges from create-time isolation settings.

Line 506 hardcodes newPidNs: true, Line 495 always builds cgroup, and restart does not apply seccomp selection used in create. This can change runtime guarantees and break restart on constrained hosts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/vm.ts` around lines 495 - 509, The restart path is overriding
create-time isolation: stop hardcoding newPidNs: true and always rebuilding
cgroup; instead have spawnAndWait respect the original isolation stored in the
VM state (use the result of buildCgroupConfig only when creating and reuse
stored cgroup config on restart), and apply the same seccomp profile selection
used at create time when calling jailer.spawn; update spawnAndWait to read the
VM state's saved flags (e.g., savedNewPidNs, savedCgroupConfig,
savedSeccompProfile) and pass those to jailer.spawn (preserve use of
buildCgroupConfig for first-boot but reuse saved values for restarts).
🧹 Nitpick comments (1)
src/stores/memory.ts (1)

20-24: Preserve clone isolation in update().

Line 23 assigns updates directly, which can retain external nested object references. Clone updates before merge to keep in-memory state isolated from caller mutations.

♻️ Suggested change
   update(id: string, updates: Partial<VmState>): void {
     const state = this.states.get(id);
     if (!state) throw vmStateNotFoundError(id);
-    Object.assign(state, updates);
+    Object.assign(state, structuredClone(updates));
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/memory.ts` around lines 20 - 24, The update method in memory store
(update(id: string, updates: Partial<VmState>)) currently merges the
caller-supplied updates by reference, risking shared nested objects; create a
deep clone of the updates before merging to preserve isolation (e.g., use
structuredClone(updates) or a project deepClone utility) and then
Object.assign(state, clonedUpdates) after verifying the state exists via
this.states.get(id) and vmStateNotFoundError(id).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/commands/start.ts`:
- Around line 28-33: When a command returns result.success === false but
result.error is undefined, the code sets process.exitCode and emits cmdLog but
never routes through handleCommandError; update the failure branch in
startCommand handling (the block checking result.success) to call
handleCommandError with a meaningful Error (or the existing result.error if
present) before setting process.exitCode and calling cmdLog.emit(), so all
failed commands consistently go through handleCommandError for centralized
logging/cleanup.

---

Duplicate comments:
In `@src/services/vm.ts`:
- Around line 427-439: The cleanup uses persisted state paths (state.chrootDir,
state.apiSocket) to build vmRootCandidates and then calls rmSync in
removeStaleFirecrackerFiles (and calls cleanupChroot elsewhere) without
validating them; call assertManagedPath(...) on state.chrootDir, state.apiSocket
and on each derived path in vmRootCandidates before performing any rmSync or
cleanupChroot operations to ensure they are within the managed root, and
bail/throw if validation fails so no rmSync runs on unvalidated/tainted paths.
- Around line 495-509: The restart path is overriding create-time isolation:
stop hardcoding newPidNs: true and always rebuilding cgroup; instead have
spawnAndWait respect the original isolation stored in the VM state (use the
result of buildCgroupConfig only when creating and reuse stored cgroup config on
restart), and apply the same seccomp profile selection used at create time when
calling jailer.spawn; update spawnAndWait to read the VM state's saved flags
(e.g., savedNewPidNs, savedCgroupConfig, savedSeccompProfile) and pass those to
jailer.spawn (preserve use of buildCgroupConfig for first-boot but reuse saved
values for restarts).

---

Nitpick comments:
In `@src/stores/memory.ts`:
- Around line 20-24: The update method in memory store (update(id: string,
updates: Partial<VmState>)) currently merges the caller-supplied updates by
reference, risking shared nested objects; create a deep clone of the updates
before merging to preserve isolation (e.g., use structuredClone(updates) or a
project deepClone utility) and then Object.assign(state, clonedUpdates) after
verifying the state exists via this.states.get(id) and vmStateNotFoundError(id).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7bf5582 and 22c95fa.

📒 Files selected for processing (7)
  • src/commands/start.ts
  • src/hooks.ts
  • src/index.ts
  • src/lib/utils.ts
  • src/lib/vm-state.ts
  • src/services/vm.ts
  • src/stores/memory.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/index.ts

Comment thread src/commands/start.ts
Comment on lines +28 to 33
if (!result.success) {
if (result.error) throw result.error;
process.exitCode = 1;
cmdLog.emit();
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid silent failure when result.success === false.

If result.error is absent, the command currently exits without routing through handleCommandError, which can hide root cause details.

🔧 Suggested change
       if (!result.success) {
-        if (result.error) throw result.error;
-        process.exitCode = 1;
-        cmdLog.emit();
-        return;
+        throw result.error ?? new Error(`Failed to start VM: ${vmId}`);
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!result.success) {
if (result.error) throw result.error;
process.exitCode = 1;
cmdLog.emit();
return;
}
if (!result.success) {
throw result.error ?? new Error(`Failed to start VM: ${vmId}`);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/start.ts` around lines 28 - 33, When a command returns
result.success === false but result.error is undefined, the code sets
process.exitCode and emits cmdLog but never routes through handleCommandError;
update the failure branch in startCommand handling (the block checking
result.success) to call handleCommandError with a meaningful Error (or the
existing result.error if present) before setting process.exitCode and calling
cmdLog.emit(), so all failed commands consistently go through handleCommandError
for centralized logging/cleanup.

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.

1 participant