Fix EISDIR warnings and Max Stack Size errors for issue #21527 - #25444
Fix EISDIR warnings and Max Stack Size errors for issue #21527#25444ProthamD wants to merge 9 commits into
Conversation
🛑 Action Required: Evaluation ApprovalSteering changes have been detected in this PR. To prevent regressions, a maintainer must approve the evaluation run before this PR can be merged. Maintainers:
Once approved, the evaluation results will be posted here automatically. |
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the robustness and stability of the CLI by addressing two critical failure modes. It improves file system interaction logic to correctly distinguish between files and directories, preventing erroneous processing. Additionally, it introduces a safeguard against excessive recursion in command processing, ensuring the application remains stable even with complex or deeply nested inputs. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request updates the .gitignore file and improves file handling logic in the core package by verifying file types during existence and binary checks. It also introduces a depth parameter in the CLI's command processor to mitigate potential recursion issues; however, the current implementation is incomplete as the depth is never incremented or passed to subsequent calls, rendering the guard ineffective.
| if (depth > 2) { | ||
| return { processedQuery: [{ text: query }] }; | ||
| } |
There was a problem hiding this comment.
The depth parameter and the check if (depth > 2) have been added to prevent a RangeError: Maximum call stack size exceeded, but the implementation appears incomplete. There is no recursive call to handleAtCommand within this function, nor is the depth parameter incremented and passed to any other function that might call it back. Consequently, depth will always remain at its default value of 0, and the check will never trigger. If the recursion is intended to happen within this function (e.g., to expand nested @ commands in the content of read files), the recursive call is missing. If the recursion happens in the caller, the caller must be updated to pass an incremented depth value.
References
- A recursive error/reconnect handler is acceptable as long as it includes a mechanism to limit the maximum number of retry attempts to prevent infinite loops.
…gs nested within the actual returned text
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces recursive @ command processing with a depth limit to handle nested file references and improves file system robustness by explicitly checking for file types before processing. I have no feedback to provide.
|
/gemini check if the code is safe |
|
The changes in this pull request are designed to improve the robustness and stability of the CLI by addressing two specific, identified failure modes: EISDIR errors during file processing and stack overflow errors during recursive command expansion. Safety Assessment
ConclusionThe changes are targeted, follow standard defensive programming practices, and directly address the issues described in the PR. The logic is sound and does not introduce obvious security vulnerabilities or performance regressions. |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces recursive processing for '@' commands with a depth limit to handle nested file references. It also refines file validation in 'read-many-files.ts' and implements a custom binary file detection logic in 'fileUtils.ts'. A critical issue was identified in the 'isBinaryFile' implementation where an undeclared variable and a missing file handle closure could lead to race conditions and resource leaks.
| fh = await fs.promises.open(filePath, 'r'); | ||
| const stats = await fh.stat(); | ||
| if (stats.isDirectory()) return false; | ||
| const fileSize = stats.size; | ||
| if (fileSize === 0) return false; // empty is not binary | ||
|
|
||
| // Sample up to 4KB from the head (previous behavior) | ||
| const sampleSize = Math.min(4096, fileSize); | ||
| const buf = Buffer.alloc(sampleSize); | ||
| const { bytesRead } = await fh.read(buf, 0, sampleSize, 0); | ||
| if (bytesRead === 0) return false; | ||
|
|
||
| // BOM → text (avoid false positives for UTF‑16/32 with nulls) | ||
| const bom = detectBOM(buf.subarray(0, Math.min(4, bytesRead))); | ||
| if (bom) return false; | ||
|
|
||
| let nonPrintableCount = 0; | ||
| for (let i = 0; i < bytesRead; i++) { | ||
| if (buf[i] === 0) return true; // strong indicator of binary when no BOM | ||
| if (buf[i] < 9 || (buf[i] > 13 && buf[i] < 32)) { | ||
| nonPrintableCount++; | ||
| } | ||
| } | ||
| // If >30% non-printable characters, consider it binary | ||
| return nonPrintableCount / bytesRead > 0.3; |
There was a problem hiding this comment.
The isBinaryFile function contains a critical file descriptor leak and a race condition. The file handle opened with fs.promises.open is not properly closed, which can exhaust file descriptors and lead to a Denial of Service (DoS). Additionally, the variable fh is used without being declared, making it a global variable. In an async function, this creates a race condition where concurrent calls can overwrite fh, leading to unpredictable behavior and potential crashes. To remediate, declare fh as a local variable and ensure it is always closed using a try...finally block. This aligns with repository rules regarding avoiding global state for concurrency and using asynchronous file system operations.
export async function isBinaryFile(filePath: string): Promise<boolean> {
let fh;
try {
fh = await fs.promises.open(filePath, 'r');
const stats = await fh.stat();
if (stats.isDirectory()) return false;
const fileSize = stats.size;
if (fileSize === 0) return false; // empty is not binary
// Sample up to 4KB from the head (previous behavior)
const sampleSize = Math.min(4096, fileSize);
const buf = Buffer.alloc(sampleSize);
const { bytesRead } = await fh.read(buf, 0, sampleSize, 0);
if (bytesRead === 0) return false;
// BOM → text (avoid false positives for UTF‑16/32 with nulls)
const bom = detectBOM(buf.subarray(0, Math.min(4, bytesRead)));
if (bom) return false;
let nonPrintableCount = 0;
for (let i = 0; i < bytesRead; i++) {
if (buf[i] === 0) return true; // strong indicator of binary when no BOM
if (buf[i] < 9 || (buf[i] > 13 && buf[i] < 32)) {
nonPrintableCount++;
}
}
// If >30% non-printable characters, consider it binary
return nonPrintableCount / bytesRead > 0.3;
} catch (error) {
debugLogger.warn(
`Failed to check if file is binary: ${filePath}`,
error instanceof Error ? error.message : String(error),
);
return false;
} finally {
if (fh) {
await fh.close();
}
}
}References
- Avoid module-level global variables for state to prevent race conditions and memory issues in concurrent environments.
- Use asynchronous file system operations (e.g., fs.promises) instead of synchronous ones to avoid blocking the event loop.
c9dc42a to
96a80eb
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces recursive @ command processing within the CLI, limited to a depth of two to prevent infinite recursion. It also replaces the external 'isbinaryfile' dependency with a custom heuristic that analyzes the first 4KB of a file for non-printable characters and UTF-8 validity. Additionally, file existence checks in the core package were updated to specifically verify file types. I have no feedback to provide.
|
This PR hasn't been updated in 7 days. Is it still active? |
|
@gundermanc hello , yes it is still active , I was waiting for a maintainer to review it, this week I got a bit busy so I didn't check for any updates or so, But it is active yes |
|
/gemini review and check if this recent commit fix the issue |
There was a problem hiding this comment.
Code Review
This pull request implements recursive expansion for '@' commands in the CLI, limited to a depth of two, and replaces the external 'isbinaryfile' dependency with a custom binary detection heuristic. Feedback suggests that mocking 'onDebugMessage' in nested calls hinders observability and should be replaced with the actual logging function. Furthermore, the manual re-implementation of binary detection is flagged as a maintenance risk and potential security concern; reviewers recommend reverting to the 'isbinaryfile' library while incorporating directory checks and robust path validation to mitigate path traversal vulnerabilities.
| import path from 'node:path'; | ||
| import type { PartUnion } from '@google/genai'; | ||
| import { isBinaryFile as isBinaryFileCheck } from 'isbinaryfile'; | ||
| import mime from 'mime/lite'; |
There was a problem hiding this comment.
The isbinaryfile package should be retained as it provides a battle-tested heuristic for binary detection. The EISDIR issue can be addressed more simply and safely by adding a directory check before calling the library function, rather than re-implementing the entire heuristic manually.
| import mime from 'mime/lite'; | |
| import { isBinaryFile as isBinaryFileCheck } from 'isbinaryfile'; |
| let fh; | ||
| try { | ||
| fh = await fs.promises.open(filePath, 'r'); | ||
| const stats = await fh.stat(); | ||
| if (stats.isDirectory()) return false; | ||
| const fileSize = stats.size; | ||
| if (fileSize === 0) return false; // empty is not binary | ||
|
|
||
| // Sample up to 4KB from the head | ||
| const sampleSize = Math.min(4096, fileSize); | ||
| const buf = Buffer.alloc(sampleSize); | ||
| const { bytesRead } = await fh.read(buf, 0, sampleSize, 0); | ||
| if (bytesRead === 0) return false; | ||
|
|
||
| // BOM → text (avoid false positives for UTF‑16/32 with nulls) | ||
| const bom = detectBOM(buf.subarray(0, Math.min(4, bytesRead))); | ||
| if (bom) return false; | ||
|
|
||
| let nonPrintableCount = 0; | ||
| let i = 0; | ||
| while (i < bytesRead) { | ||
| const byte = buf[i]; | ||
| if (byte === 0) return true; // null byte → strong binary signal | ||
| if (byte < 9 || (byte > 13 && byte < 32)) { | ||
| nonPrintableCount++; | ||
| i++; | ||
| } else if (byte >= 0x80) { | ||
| // Determine expected UTF-8 sequence length from the lead byte | ||
| let seqLen = 0; | ||
| if (byte >= 0xf0 && byte <= 0xf7) seqLen = 4; | ||
| else if (byte >= 0xe0 && byte <= 0xef) seqLen = 3; | ||
| else if (byte >= 0xc2 && byte <= 0xdf) seqLen = 2; | ||
| // 0x80–0xBF are continuation bytes without a leading byte → invalid | ||
| // 0xC0–0xC1 are overlong encodings → invalid | ||
| // 0xF8–0xFF are invalid UTF-8 | ||
|
|
||
| if (seqLen > 0 && i + seqLen <= bytesRead) { | ||
| // Verify continuation bytes (0x80–0xBF) | ||
| let valid = true; | ||
| for (let j = 1; j < seqLen; j++) { | ||
| if ((buf[i + j] & 0xc0) !== 0x80) { | ||
| valid = false; | ||
| break; | ||
| } | ||
| } | ||
| if (valid) { | ||
| i += seqLen; // skip valid multi-byte sequence | ||
| } else { | ||
| nonPrintableCount++; | ||
| i++; | ||
| } | ||
| } else { | ||
| // lone continuation byte, partial sequence, or invalid lead byte | ||
| nonPrintableCount++; | ||
| i++; | ||
| } | ||
| } else { | ||
| i++; | ||
| } | ||
| } | ||
| // If >30% non-printable characters, consider it binary | ||
| return nonPrintableCount / bytesRead > 0.3; | ||
| } finally { | ||
| if (fh) await fh.close(); | ||
| } |
There was a problem hiding this comment.
Re-implementing the binary detection heuristic adds significant complexity and risk of regressions. A more maintainable approach is to use the established isbinaryfile library after verifying that the path is not a directory. Furthermore, as this is a utility function performing file system operations, it must validate path inputs internally (e.g., using a robust function like resolveToRealPath) to prevent path traversal vulnerabilities.
const stats = await fsPromises.stat(filePath);
if (stats.isDirectory()) return false;
return await isBinaryFileCheck(filePath);References
- Ensure consistent path resolution by using a single, robust function (e.g.,
resolveToRealPath) for all related path validations. - Utility functions that perform file system operations should validate their path inputs internally to prevent path traversal vulnerabilities, rather than relying solely on callers to perform validation.
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
|
Right , I think isbinaryfile should be there,replacing it to fix this would be a bad choice I will fix the logic by tomorrow and update this PR |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces support for nested @ commands within file content in the CLI, implementing a recursion depth limit of 2 to prevent infinite loops. It also refines file system checks in the core package by using fs.stat to specifically verify file types and resolving real paths before binary file detection. I have no feedback to provide.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces recursive processing for '@' commands within the CLI, enabling the resolution of nested file references up to a depth of two. Additionally, it refines file system operations by replacing existence checks with explicit file status checks in 'read-many-files.ts' and enhancing 'isBinaryFile' to handle directory paths and resolve real paths. I have no feedback to provide as there were no review comments to evaluate.
|
@gundermanc hello, i have updated this PR to the latest, Can you please review it if possible? |
|
This doesn't compile |
|
Okey lemme chwck |
|
/gemini review |
|
@scidomino I have fixed the issue and tested it locally it passed all 59 tests of the file from which the compilation error was occurrig |
There was a problem hiding this comment.
Code Review
This pull request introduces recursive processing for '@' commands with a depth limit of 2 and enhances file validation in utility functions. Changes in 'read-many-files.ts' and 'fileUtils.ts' ensure that only regular files are processed. A security-related improvement was suggested for 'isBinaryFile' to prevent potential Denial of Service attacks by verifying that a path is a regular file before reading, which avoids blocking on special files like named pipes.
| return await isBinaryFileCheck(filePath); | ||
| const realPath = resolveToRealPath(filePath); | ||
| const stats = await fsPromises.stat(realPath); | ||
| if (stats.isDirectory()) return false; |
There was a problem hiding this comment.
A Denial of Service (DoS) vulnerability exists in the isBinaryFile function. If the resolved path points to a special file (e.g., named pipe, device file) and is not verified as a regular file before being passed to isBinaryFileCheck, the subsequent read operation could block indefinitely. Additionally, ensure you use asynchronous file system operations (e.g., fs.promises.realpath) instead of synchronous ones to avoid blocking the event loop. This utility should validate its path inputs internally and continue to use resolveToRealPath for consistent path resolution.
| if (stats.isDirectory()) return false; | |
| if (!stats.isFile()) return false; |
References
- Use asynchronous file system operations (e.g., fs.promises.readFile) instead of synchronous ones (e.g., fs.readFileSync) to avoid blocking the event loop.
- Utility functions that perform file system operations should validate their path inputs internally to prevent path traversal vulnerabilities, rather than relying solely on callers to perform validation.
- Ensure consistent path resolution by using a single, robust function (e.g., resolveToRealPath) for all related path validations, including internal validations in components like WorkspaceContext.
|
I think this PR should be dropped and you should start over. We definitely DO NOT want recursive expansion (where an at-included file might have it's own at-includes). Instead, just fix the two specific issues without introducing new functionality. |
Description
Fixes #21527.
This PR addresses two critical failure modes in the CLI related to large inputs and glob configurations:
EISDIR Warning Error (
isBinaryFile)The
read-many-filestool usesglob, which sometimes returned literal dictionary paths if they were inadvertently treated as existant files. By switching fromfsPromises.access()tofsPromises.stat()and correctly using.isFile(), this limits the initial inclusion glob pool to files only. A subsequent checkif (stats.isDirectory()) return false;adds an extra layer of protection aroundisBinaryFile.RangeError: Maximum call stack size exceededhandleAtCommandin theatCommandProcessor.tscould recursively invoke itself on nested path lookups when@commands returned file text that itself included further@symbols, eventually exceeding the stack limit on deep, cyclic paths. Now adepthparameter truncates expansion past a safe threshold (a depth of2).These safeguards make the execution significantly more robust when dealing with generic patterns and large text buffers.
Testing
Tested manually with problematic inclusion paths and deeply nested queries. Unit tests verified.