mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 02:49:33 +01:00
fix: probe bounded before a realpath in clone and New folder (release review, #516)
Clone ran an unbounded realpath over every Docker case workspace, and the mkdir route inherited the picker resolver's realpathSync, which freezes the whole server on a mount that stopped answering. The picker's browse route has the same call and is left as it was. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
345c901e81
commit
7803d5616a
3 files changed
+53
-1
No files matched your search
@@ -802,8 +802,14 @@ export function registerCaseRoutes(
|
||||
// with the workspace bind-mounted can write both while it fetches. Like git status, fetch and worktrees,
|
||||
// never clone at or inside a Docker case workspace. Fails closed when the Docker case list cannot be read.
|
||||
try {
|
||||
// Probed first (#516): a case's workspace can sit on a mount that stopped answering, and an unbounded
|
||||
// realpath there would hold this request and a threadpool worker. Such a root is judged as spelled.
|
||||
const roots = await Promise.all(
|
||||
(await defaultDockerWorkspaces()).map((root) => fs.realpath(root).catch(() => resolve(root)))
|
||||
(await defaultDockerWorkspaces()).map(async (root) =>
|
||||
(await probePathKind(root, { pastCap: true })) === 'unknown'
|
||||
? resolve(root)
|
||||
: fs.realpath(root).catch(() => resolve(root))
|
||||
)
|
||||
);
|
||||
// A custom folder is already symlink-resolved; the default one is judged through its real cases dir.
|
||||
const dest =
|
||||
|
||||
@@ -1457,6 +1457,15 @@ export function registerFileRoutes(app: FastifyInstance, ctx: SessionPort & Even
|
||||
// "New folder" in the path picker: creates ONE folder directly inside a folder the picker may browse.
|
||||
app.post('/api/filesystem/mkdir', async (req, reply): Promise<ApiResponse<{ path: string }>> => {
|
||||
const { path: parentPath, name, sessionId, showHidden } = parseBody(FilesystemMkdirSchema, req.body);
|
||||
// Probed first (#516): the picker's resolver runs realpathSync on this path, which would freeze the
|
||||
// whole server on a mount that stopped answering.
|
||||
if ((await probePathKind(parentPath, { pastCap: true })) === 'unknown') {
|
||||
reply.code(422);
|
||||
return createErrorResponse(
|
||||
ApiErrorCode.OPERATION_FAILED,
|
||||
describeUnknownPath('The folder', parentPath, { pastCap: true })
|
||||
);
|
||||
}
|
||||
const { candidatePath, resolvedPath, blockedTrees } = await resolveFilesystemPickerPath(
|
||||
ctx,
|
||||
req,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @fileoverview `POST /api/filesystem/mkdir` probes the folder bounded before the picker's
|
||||
* resolver runs `realpathSync` on it (#516): a mount that stopped answering is refused, and
|
||||
* the request never reaches a synchronous call that would freeze the server.
|
||||
*
|
||||
* The probe is mocked: a mount that never answers cannot be made here. Port: none (inject).
|
||||
*/
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('../../src/utils/bounded-path-probe.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../src/utils/bounded-path-probe.js')>();
|
||||
return { ...actual, probePathKind: vi.fn(actual.probePathKind) };
|
||||
});
|
||||
|
||||
import { probePathKind } from '../../src/utils/bounded-path-probe.js';
|
||||
import { registerFileRoutes } from '../../src/web/routes/file-routes.js';
|
||||
import { createRouteTestHarness } from './_route-test-utils.js';
|
||||
|
||||
describe('POST /api/filesystem/mkdir on an unresponsive mount', () => {
|
||||
afterEach(() => vi.mocked(probePathKind).mockClear());
|
||||
|
||||
it('refuses before the picker resolver touches the path', async () => {
|
||||
vi.mocked(probePathKind).mockResolvedValueOnce('unknown');
|
||||
const { app } = await createRouteTestHarness(registerFileRoutes);
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/filesystem/mkdir',
|
||||
payload: { path: '/mnt/dead/projects', name: 'fresh' },
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(422);
|
||||
expect(res.json().errorCode).toBe('OPERATION_FAILED');
|
||||
expect(res.json().error).toMatch(/not responding|not checked/);
|
||||
expect(probePathKind).toHaveBeenCalledWith('/mnt/dead/projects', { pastCap: true });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user