Compare commits

...
Author SHA1 Message Date
bincxz 88d8c69ebd fix(ssh): validate lsof cwd diagnostics against real paths 2026-09-25 15:48:13 +08:00
netcatty-bot 59d47e91d6 fix(#3493): automated AI fix 2026-09-23 03:21:16 +00:00
2 changed files with 74 additions and 6 deletions

No files matched your search

+46 -3
View File
@@ -635,13 +635,56 @@ function createSessionOpsApi(ctx) {
_rc_cwd=$(readlink "/proc/$1/cwd" 2>/dev/null)
if [ -n "$_rc_cwd" ]; then printf '%s\\n' "$_rc_cwd"; return 0; fi
if command -v lsof >/dev/null 2>&1; then
# An unknown-type record can put a readlink error in its name field.
# Only a confirmed directory name is usable as an upload destination.
# lsof can label an unreadable root shell's cwd as a directory while
# appending "(readlink: Permission denied)" to its name on AL2023
# (#3493). Check annotation-shaped names before using them: real
# directories can also end in text such as " (owner: alice)".
_rc_cwd=$(LC_ALL=C lsof -a -p "$1" -d cwd -Fnt 2>/dev/null | awk '
/^f/ { is_dir=0 }
/^t/ { is_dir=($0 == "tDIR" || $0 == "tVDIR") }
/^n/ && is_dir { print substr($0, 2); exit }
/^n/ && is_dir {
name=substr($0, 2)
if (name ~ / \\([A-Za-z][A-Za-z0-9]*: .*\\)$/) print "?" name
else print name
exit
}
')
case "$_rc_cwd" in
\\?*)
_rc_cwd=\${_rc_cwd#?}
# lsof escapes non-ASCII bytes under LC_ALL=C. Decode only for
# the existence check; keep the original for the client decoder.
_rc_test_cwd=$(printf '%s' "$_rc_cwd" | LC_ALL=C awk '
function hex(c) { return index("0123456789abcdef", tolower(c)) - 1 }
{
for (i=1; i<=length($0); i++) {
c=substr($0,i,1)
if (c!="\\\\") { printf "%s", c; continue }
e=substr($0,++i,1)
if (e=="x") {
hi=hex(substr($0,++i,1)); lo=hex(substr($0,++i,1))
if (hi<0 || lo<0) exit 1
printf "%c", hi*16+lo
} else if (e=="\\\\") printf "\\\\"
else if (e=="n") printf "\\n"
else if (e=="r") printf "\\r"
else if (e=="t") printf "\\t"
else if (e=="b") printf "%c", 8
else if (e=="f") printf "%c", 12
else if (e=="v") printf "%c", 11
else if (e ~ /^[0-7]$/) {
n=e+0
for (j=0; j<2 && substr($0,i+1,1) ~ /^[0-7]$/; j++) {
n=n*8+substr($0,++i,1)
}
printf "%c", n
} else exit 1
}
}
') || return 1
[ -d "$_rc_test_cwd" ] || return 1
;;
esac
if [ -n "$_rc_cwd" ]; then printf 'NETCATTY_LSOF_CWD=%s\\n' "$_rc_cwd"; return 0; fi
fi
return 1
@@ -1,6 +1,9 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const { EventEmitter } = require("node:events");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { createSessionOpsApi, decodeLsofFileName } = require("./sessionOps.cjs");
@@ -123,7 +126,7 @@ test("session cwd probe decodes the marked lsof pathname", async () => {
assert.deepEqual(result, { success: true, cwd: "/srv/中文" });
});
test("lsof cwd fallback accepts only directory records, not diagnostic names (#3237)", async () => {
test("lsof cwd fallback accepts only directory records, not diagnostic names (#3237, #3493)", async (t) => {
let script;
const api = makeApi({
shellPid: "4242",
@@ -144,13 +147,35 @@ test("lsof cwd fallback accepts only directory records, not diagnostic names (#3
const helper = script.match(/ read_shell_cwd\(\) \{[\s\S]*?\n \}/)?.[0];
assert.ok(helper, "exercise the actual remote cwd helper");
const { spawnSync } = require("node:child_process");
const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "netcatty-lsof-cwd-"));
t.after(() => fs.rmSync(fixtureRoot, { recursive: true, force: true }));
const realParenthesizedPath = path.join(fixtureRoot, "release (owner: alice)");
const realDiagnosticShapedPath = path.join(fixtureRoot, "job (stat: ready)");
const realErrorShapedPath = path.join(fixtureRoot, "job (readlink: Permission denied)");
const realUnicodePath = path.join(fixtureRoot, "中文 (owner: alice)");
fs.mkdirSync(realParenthesizedPath);
fs.mkdirSync(realDiagnosticShapedPath);
fs.mkdirSync(realErrorShapedPath);
fs.mkdirSync(realUnicodePath);
for (const [output, expected] of [
["p4242\nfcwd\ntunknown\nn/proc/4242/cwd (readlink: Permission denied)\n", null],
["p4242\nfcwd\ntunknown\nn/proc/4242/cwd (readlink: No such file or directory)\n", null],
// AL2023: lsof reports the VDIR type from the fd but annotates the name
// when the login user cannot readlink a root-owned shell's cwd (#3493).
["p683339\nfcwd\ntVDIR\nn/proc/683339/cwd (readlink: Permission denied)\n", null],
["p4242\nfcwd\ntDIR\nn/proc/4242/cwd (readlink: Permission denied)\n", null],
["p4242\nfcwd\ntDIR\nn/proc/4242/cwd (readlink: No such file or directory)\n", null],
["p4242\nfcwd\ntDIR\nn/proc/4242/cwd (stat: Operation not permitted)\n", null],
["p4242\nfcwd\ntDIR\nn/srv/app (stat: Operation not permitted)\n", null],
["p4242\nfcwd\ntDIR\nn/srv/app (readlink: Permission denied)\n", null],
["p4242\nfcwd\nn/proc/4242/cwd\n", null],
["p4242\nfcwd\ntDIR\nn/srv/app\n", "/srv/app"],
["p4242\nfcwd\ntVDIR\nn/usr/home/alice\n", "/usr/home/alice"],
["p4242\nfcwd\ntDIR\nn/tmp/literal (readlink: Permission denied)\n", "/tmp/literal (readlink: Permission denied)"],
["p4242\nfcwd\ntDIR\nn/srv/backup (old)\n", "/srv/backup (old)"],
["p4242\nfcwd\ntDIR\nn" + realParenthesizedPath + "\n", realParenthesizedPath],
["p4242\nfcwd\ntDIR\nn" + realDiagnosticShapedPath + "\n", realDiagnosticShapedPath],
["p4242\nfcwd\ntDIR\nn" + realErrorShapedPath + "\n", realErrorShapedPath],
["p4242\nfcwd\ntDIR\nn" + realUnicodePath.replace("中文", "\\xe4\\xb8\\xad\\xe6\\x96\\x87") + "\n", realUnicodePath.replace("中文", "\\xe4\\xb8\\xad\\xe6\\x96\\x87")],
["p4242\nfcwd\ntDIR\nn/tmp/\\xe4\\xb8\\xad\\xe6\\x96\\x87\n", "/tmp/\\xe4\\xb8\\xad\\xe6\\x96\\x87"],
]) {
const result = spawnSync("sh", ["-c", `
@@ -159,7 +184,7 @@ test("lsof cwd fallback accepts only directory records, not diagnostic names (#3
${helper}
read_shell_cwd 4242
`], { encoding: "utf8", env: { ...process.env, LSOF_TEST_OUTPUT: output } });
assert.equal(result.status, expected === null ? 1 : 0, output);
assert.equal(result.status, expected === null ? 1 : 0, `${output}\n${result.stderr}`);
assert.equal(result.stdout, expected === null ? "" : `NETCATTY_LSOF_CWD=${expected}\n`, output);
}
});