mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
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:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user