From 38e32db4b76cb542e3bfe703f9ad65eebde2d176 Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Mon, 29 Sep 2025 18:48:05 -0700 Subject: [PATCH 1/2] Fix infinite ssh config file processing. --- Extension/src/SSH/sshHosts.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Extension/src/SSH/sshHosts.ts b/Extension/src/SSH/sshHosts.ts index 97d3b625c..8af55465e 100644 --- a/Extension/src/SSH/sshHosts.ts +++ b/Extension/src/SSH/sshHosts.ts @@ -99,7 +99,15 @@ export async function getSshConfiguration(configurationPath: string, resolveIncl return config; } +function getProcessedPathKey(filePath: string): string { + const absolutePath: string = path.resolve(filePath); + const normalizedPath: string = path.normalize(absolutePath); + return isWindows ? normalizedPath.toLowerCase() : normalizedPath; +} + async function resolveConfigIncludes(config: Configuration, configPath: string): Promise { + const processedIncludePaths: Set = new Set(); + processedIncludePaths.add(getProcessedPathKey(configPath)); for (const entry of config) { if (isDirective(entry) && entry.param === 'Include') { let includePath: string = resolveHome(entry.value); @@ -114,6 +122,11 @@ async function resolveConfigIncludes(config: Configuration, configPath: string): const pathsToGetFilesFrom: string[] = await globAsync(includePath); for (const filePath of pathsToGetFilesFrom) { + const includeKey: string = getProcessedPathKey(filePath); + if (processedIncludePaths.has(includeKey)) { + continue; + } + processedIncludePaths.add(includeKey); await getIncludedConfigFile(config, filePath); } } From 732f1cdb8ba2130297a7bc91f5f049d117bdec1e Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Mon, 29 Sep 2025 19:56:32 -0700 Subject: [PATCH 2/2] Fix. --- Extension/src/SSH/sshHosts.ts | 64 ++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/Extension/src/SSH/sshHosts.ts b/Extension/src/SSH/sshHosts.ts index 8af55465e..98c4d9308 100644 --- a/Extension/src/SSH/sshHosts.ts +++ b/Extension/src/SSH/sshHosts.ts @@ -105,35 +105,58 @@ function getProcessedPathKey(filePath: string): string { return isWindows ? normalizedPath.toLowerCase() : normalizedPath; } -async function resolveConfigIncludes(config: Configuration, configPath: string): Promise { - const processedIncludePaths: Set = new Set(); - processedIncludePaths.add(getProcessedPathKey(configPath)); - for (const entry of config) { - if (isDirective(entry) && entry.param === 'Include') { - let includePath: string = resolveHome(entry.value); - if (isWindows && !!includePath.match(/^\/[a-z]:/i)) { - includePath = includePath.substr(1); - } +async function resolveConfigIncludes( + config: Configuration, + configPath: string, + processedIncludePaths?: Set, + processedIncludeEntries?: WeakSet +): Promise { + processedIncludePaths = processedIncludePaths ?? new Set(); + processedIncludeEntries = processedIncludeEntries ?? new WeakSet(); + const configKey: string = getProcessedPathKey(configPath); + if (processedIncludePaths.has(configKey)) { + return; + } + processedIncludePaths.add(configKey); + try { + for (const entry of config) { + if (isDirective(entry) && entry.param === 'Include') { + // Prevent duplicate expansion of the same Include directive within a single resolution pass. + if (processedIncludeEntries.has(entry)) { + continue; + } + processedIncludeEntries.add(entry); + let includePath: string = resolveHome(entry.value); + if (isWindows && !!includePath.match(/^\/[a-z]:/i)) { + includePath = includePath.slice(1); + } - if (!path.isAbsolute(includePath)) { - includePath = path.resolve(path.dirname(configPath), includePath); - } + if (!path.isAbsolute(includePath)) { + includePath = path.resolve(path.dirname(configPath), includePath); + } - const pathsToGetFilesFrom: string[] = await globAsync(includePath); + const pathsToGetFilesFrom: string[] = await globAsync(includePath); - for (const filePath of pathsToGetFilesFrom) { - const includeKey: string = getProcessedPathKey(filePath); - if (processedIncludePaths.has(includeKey)) { - continue; + for (const filePath of pathsToGetFilesFrom) { + const includeKey: string = getProcessedPathKey(filePath); + if (processedIncludePaths.has(includeKey)) { + continue; + } + await getIncludedConfigFile(config, filePath, processedIncludePaths, processedIncludeEntries); } - processedIncludePaths.add(includeKey); - await getIncludedConfigFile(config, filePath); } } + } finally { + processedIncludePaths.delete(configKey); } } -async function getIncludedConfigFile(config: Configuration, includePath: string): Promise { +async function getIncludedConfigFile( + config: Configuration, + includePath: string, + processedIncludePaths: Set, + processedIncludeEntries: WeakSet +): Promise { let includedContents: string; try { includedContents = (await fs.readFile(includePath)).toString(); @@ -149,6 +172,7 @@ async function getIncludedConfigFile(config: Configuration, includePath: string) getSshChannel().appendLine(localize("failed.to.parse.SSH.config", "Failed to parse SSH configuration file {0}: {1}", includePath, (err as Error).message)); return; } + await resolveConfigIncludes(parsedIncludedContents, includePath, processedIncludePaths, processedIncludeEntries); config.push(...parsedIncludedContents); }