fix(plugins): bound plugin manifest metadata file reads (#110036)

* fix(plugins): bound plugin manifest metadata file reads

* fix(plugins): use correct Error type check in oversized manifest catch

readRegularFileSync throws a plain Error (not RangeError) when the file
exceeds maxBytes. Change the catch-block type check from RangeError to
Error to properly detect oversized plugin manifests and emit the
subsystem warning.

* test(plugins): exercise valid oversized manifests

Co-authored-by: 陈宪彪0668000387 <chen.xianbiao@xydigit.com>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
cxbAsDev
2026-07-18 18:22:30 +08:00
committed by GitHub
parent 21c3438ded
commit 2e21d53c84
2 changed files with 70 additions and 2 deletions
@@ -126,4 +126,55 @@ describe("listOpenClawPluginManifestMetadata", () => {
origin: "global",
});
});
it("skips oversized plugin manifests to prevent OOM during metadata scan", () => {
const root = createTempRoot();
const home = path.join(root, "home");
const goodPluginDir = path.join(home, ".openclaw", "extensions", "good-plugin");
writeJson(path.join(goodPluginDir, "openclaw.plugin.json"), { id: "good-plugin" });
const oversizedDir = path.join(home, ".openclaw", "extensions", "big-plugin");
const oversizedPath = path.join(oversizedDir, "openclaw.plugin.json");
fs.mkdirSync(oversizedDir, { recursive: true });
fs.writeFileSync(
oversizedPath,
JSON.stringify({ id: "big-plugin", pad: "x".repeat(256 * 1024) }),
"utf8",
);
expect(fs.statSync(oversizedPath).size).toBeGreaterThan(256 * 1024);
const records = listOpenClawPluginManifestMetadata({
OPENCLAW_HOME: home,
OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(root, "empty-bundled"),
});
// "good-plugin" is present; "big-plugin" is skipped due to oversized manifest.
expect(records.find((record) => record.manifest.id === "good-plugin")).toBeTruthy();
expect(records.find((record) => record.manifest.id === "big-plugin")).toBeUndefined();
});
it("accepts plugin manifests at the exact byte limit", () => {
const root = createTempRoot();
const home = path.join(root, "home");
const exactDir = path.join(home, ".openclaw", "extensions", "exact-plugin");
fs.mkdirSync(exactDir, { recursive: true });
// Write a compact JSON manifest padded to exactly the byte limit.
const exactPath = path.join(exactDir, "openclaw.plugin.json");
const exactManifest = { id: "exact-plugin", pad: "" };
const compactJson = JSON.stringify(exactManifest);
const requiredPadding = 256 * 1024 - Buffer.byteLength(compactJson, "utf8");
exactManifest.pad = "x".repeat(requiredPadding);
fs.writeFileSync(exactPath, JSON.stringify(exactManifest), "utf8");
expect(Buffer.byteLength(fs.readFileSync(exactPath), "utf8")).toBe(256 * 1024);
const records = listOpenClawPluginManifestMetadata({
OPENCLAW_HOME: home,
OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(root, "empty-bundled"),
});
expect(records.find((record) => record.manifest.id === "exact-plugin")).toBeTruthy();
});
});
+19 -2
View File
@@ -6,10 +6,18 @@ import { normalizeOptionalString as normalizeTrimmedString } from "@openclaw/nor
import { resolveStateDir } from "../config/paths.js";
import { resolveHomeRelativePath } from "../infra/home-dir.js";
import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
import { readRegularFileSync } from "../infra/regular-file.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { parseJsonWithJson5Fallback } from "../utils/parse-json-compat.js";
import { resolveBundledPluginsDir } from "./bundled-dir.js";
import { readPersistedInstalledPluginIndexSync } from "./installed-plugin-index-store.js";
// Plugin manifest files are small metadata descriptors. Bound reads to prevent
// a corrupted or hostile manifest from exhausting memory during metadata scan.
const PLUGIN_MANIFEST_METADATA_MAX_BYTES = 256 * 1024;
const log = createSubsystemLogger("plugins/manifest-metadata-scan");
type PluginManifestMetadataRecord = {
pluginDir: string;
manifest: Record<string, unknown>;
@@ -56,9 +64,18 @@ function listChildPluginDirs(
function readJsonObject(filePath: string): Record<string, unknown> | undefined {
try {
const parsed = parseJsonWithJson5Fallback(fs.readFileSync(filePath, "utf8"));
const { buffer } = readRegularFileSync({
filePath,
maxBytes: PLUGIN_MANIFEST_METADATA_MAX_BYTES,
});
const parsed = parseJsonWithJson5Fallback(buffer.toString("utf-8"));
return isRecord(parsed) ? parsed : undefined;
} catch {
} catch (err) {
if (err instanceof Error && err.message.includes("exceeds")) {
log.warn(
`Ignoring oversized plugin manifest at ${filePath}: file exceeds the ${PLUGIN_MANIFEST_METADATA_MAX_BYTES}-byte limit`,
);
}
return undefined;
}
}