@codyswann/lisa 4.45.7 → 4.45.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +21 -2
- package/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.mjs +68 -0
- package/all/merge/.claude/settings.json +10 -0
- package/dist/configs/vitest/scratch-global-setup.d.ts +9 -3
- package/dist/configs/vitest/scratch-global-setup.d.ts.map +1 -1
- package/dist/configs/vitest/scratch-global-setup.js +23 -10
- package/dist/configs/vitest/scratch-global-setup.js.map +1 -1
- package/dist/configs/vitest/scratch-tmpdir-trust.d.ts +48 -0
- package/dist/configs/vitest/scratch-tmpdir-trust.d.ts.map +1 -0
- package/dist/configs/vitest/scratch-tmpdir-trust.js +194 -0
- package/dist/configs/vitest/scratch-tmpdir-trust.js.map +1 -0
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +6 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +13 -8
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +5 -5
- package/plugins/lisa/.claude-plugin/plugin.json +9 -1
- package/plugins/lisa/.codex-plugin/hooks.json +8 -0
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/worktree-binding-guard.mjs +68 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +9 -1
- package/plugins/lisa-copilot/hooks/worktree-binding-guard.mjs +68 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/hooks.json +3 -0
- package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +68 -0
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/.claude-plugin/plugin.json +8 -0
- package/plugins/src/base/hooks/worktree-binding-guard.mjs +68 -0
- package/scripts/check-shell-guard-refusal-coverage.mjs +49 -3
- package/scripts/lib/shell-guard-trace.mjs +26 -8
- package/scripts/lisa-enforcement-fallback.sh +21 -2
- package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +244 -3
|
@@ -293,6 +293,14 @@
|
|
|
293
293
|
"command": "command -v entire >/dev/null 2>&1 && entire hooks claude-code session-start || true"
|
|
294
294
|
}
|
|
295
295
|
]
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
"hooks": [
|
|
299
|
+
{
|
|
300
|
+
"type": "command",
|
|
301
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/worktree-binding-guard.sh"
|
|
302
|
+
}
|
|
303
|
+
]
|
|
296
304
|
}
|
|
297
305
|
],
|
|
298
306
|
"SubagentStart": [
|
|
@@ -547,6 +547,63 @@ function claimedRoot(toolInput, cwd) {
|
|
|
547
547
|
return null;
|
|
548
548
|
}
|
|
549
549
|
|
|
550
|
+
/**
|
|
551
|
+
* Record where the session STARTED, so its first guarded call is checked
|
|
552
|
+
* against a baseline rather than establishing one.
|
|
553
|
+
*
|
|
554
|
+
* ## Why the first call could not be checked before
|
|
555
|
+
*
|
|
556
|
+
* It established the baseline instead of testing one, which is
|
|
557
|
+
* trust-on-first-use — and TOFU is only as safe as its first use. Here the
|
|
558
|
+
* first use is assigned by the harness, unasked (CodySwannGT/lisa#3864), so the
|
|
559
|
+
* one moment the guard trusted was the moment the session had least control
|
|
560
|
+
* over. A session already sitting in a foreign worktree when it first acted
|
|
561
|
+
* bound to that tree and was never refused, and every later call was then
|
|
562
|
+
* checked faithfully against the wrong baseline — defending the session INTO
|
|
563
|
+
* the wrong worktree rather than out of it (CodySwannGT/lisa#3955).
|
|
564
|
+
*
|
|
565
|
+
* ## Why an earlier observation separates what a cleverer check cannot
|
|
566
|
+
*
|
|
567
|
+
* At the first guarded call, "I was displaced before I acted" and "I
|
|
568
|
+
* legitimately started here" present identically: one session id, one observed
|
|
569
|
+
* root, no prior state. No comparison can tell them apart, because the
|
|
570
|
+
* information is not in the hook's inputs at that instant.
|
|
571
|
+
*
|
|
572
|
+
* At session start it is. The displacement mechanism actually observed is a
|
|
573
|
+
* PEER's `EnterWorktree` moving this session (CodySwannGT/lisa#3712), which
|
|
574
|
+
* happens during the session — after it has started. A baseline taken at start
|
|
575
|
+
* is therefore recorded BEFORE the event that moves it, and the first guarded
|
|
576
|
+
* call becomes an ordinary comparison against a baseline that already exists.
|
|
577
|
+
* It reaches the same refusal a later displacement produces, because by then it
|
|
578
|
+
* is one.
|
|
579
|
+
*
|
|
580
|
+
* ## What this does NOT achieve
|
|
581
|
+
*
|
|
582
|
+
* #3955 asks for a baseline from the ASSIGNER rather than from observation.
|
|
583
|
+
* That is not what this is and Lisa cannot build it: Lisa does not own
|
|
584
|
+
* `EnterWorktree`, and nothing the harness writes names a session's assigned
|
|
585
|
+
* worktree in a form a hook can read. This is still an observation, just an
|
|
586
|
+
* earlier one. It closes the window between session start and the first guarded
|
|
587
|
+
* call; it leaves open a session LAUNCHED already displaced, where start
|
|
588
|
+
* observes the foreign tree and trust-on-first-use still applies.
|
|
589
|
+
*
|
|
590
|
+
* Returns nothing on purpose, like its sibling below: SessionStart has no call
|
|
591
|
+
* to refuse, so every path through it allows.
|
|
592
|
+
* @param sessionId - The session this baseline belongs to
|
|
593
|
+
* @param observed - The worktree root the session started in
|
|
594
|
+
*/
|
|
595
|
+
function recordBaseline(sessionId, observed) {
|
|
596
|
+
// Never overwrite. A resumed session, a reconnect, or anything else that
|
|
597
|
+
// fires the event twice must not be able to launder a displaced binding into
|
|
598
|
+
// a fresh baseline — which is the whole failure this exists to close.
|
|
599
|
+
if (readState(sessionId)?.boundRoot) return;
|
|
600
|
+
writeState(sessionId, {
|
|
601
|
+
boundRoot: observed,
|
|
602
|
+
claimedRoot: null,
|
|
603
|
+
updatedAt: new Date().toISOString(),
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
550
607
|
/**
|
|
551
608
|
* Record what `EnterWorktree` just claimed, for the next tool call to check.
|
|
552
609
|
*
|
|
@@ -648,6 +705,10 @@ function evaluate(payload) {
|
|
|
648
705
|
const observed = worktreeRoot(cwd);
|
|
649
706
|
if (!observed) return 0;
|
|
650
707
|
|
|
708
|
+
if (payload.hook_event_name === "SessionStart") {
|
|
709
|
+
recordBaseline(sessionId, observed);
|
|
710
|
+
return 0;
|
|
711
|
+
}
|
|
651
712
|
if (payload.tool_name === "EnterWorktree") {
|
|
652
713
|
recordClaim(payload, observed);
|
|
653
714
|
return 0;
|
|
@@ -659,6 +720,13 @@ function evaluate(payload) {
|
|
|
659
720
|
|
|
660
721
|
const state = readState(sessionId);
|
|
661
722
|
if (!state?.boundRoot) {
|
|
723
|
+
// No baseline means SessionStart did not run for this session — an older
|
|
724
|
+
// install, a runtime that fires no such event, a harness that skips it.
|
|
725
|
+
// Absence of a baseline is absence of evidence, not evidence of
|
|
726
|
+
// displacement, so this records one exactly as it always did rather than
|
|
727
|
+
// refusing. Treating it as a refusal would turn a floor into a wall on
|
|
728
|
+
// every surface where the event does not fire, and a wall gets switched
|
|
729
|
+
// off, which costs the whole guard.
|
|
662
730
|
writeState(sessionId, {
|
|
663
731
|
boundRoot: observed,
|
|
664
732
|
claimedRoot: null,
|
|
@@ -28,9 +28,13 @@
|
|
|
28
28
|
*
|
|
29
29
|
* CLI:
|
|
30
30
|
* node scripts/check-shell-guard-refusal-coverage.mjs [--json] [--trace FILE]
|
|
31
|
+
* [--trace-out FILE]
|
|
31
32
|
*
|
|
32
33
|
* --trace FILE judge an existing trace instead of producing one. Use it in
|
|
33
34
|
* CI when the full suite already ran with the tracer imported.
|
|
35
|
+
* --trace-out FILE
|
|
36
|
+
* also write the judged trace to FILE, so a later step in the
|
|
37
|
+
* same job can read what this run observed. Never commit it.
|
|
34
38
|
*
|
|
35
39
|
* Exit codes (mirroring the sibling check-* scripts):
|
|
36
40
|
* 0 — every guard the run drove was observed refusing and allowing.
|
|
@@ -41,7 +45,7 @@
|
|
|
41
45
|
* @module scripts/check-shell-guard-refusal-coverage
|
|
42
46
|
*/
|
|
43
47
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
44
|
-
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
48
|
+
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
45
49
|
import { tmpdir } from "node:os";
|
|
46
50
|
import path from "node:path";
|
|
47
51
|
import process from "node:process";
|
|
@@ -56,6 +60,9 @@ import {
|
|
|
56
60
|
tracerIndex,
|
|
57
61
|
} from "./lib/shell-guard-refusal-coverage.mjs";
|
|
58
62
|
|
|
63
|
+
/** The flag naming a file the produced trace should also be written to. */
|
|
64
|
+
const TRACE_OUT_FLAG = "--trace-out";
|
|
65
|
+
|
|
59
66
|
/** Wall-clock ceiling for the traced vitest run. */
|
|
60
67
|
const RUN_TIMEOUT_MS = 45 * 60_000;
|
|
61
68
|
|
|
@@ -185,16 +192,54 @@ function produceTrace(root, population) {
|
|
|
185
192
|
};
|
|
186
193
|
}
|
|
187
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Persist the trace this run produced, when the caller asked for it.
|
|
197
|
+
*
|
|
198
|
+
* The trace is the only artefact that can tell a LATER step — the mutation
|
|
199
|
+
* gate's uninstrumentable branch, which until CodySwannGT/lisa#3931 asserted a
|
|
200
|
+
* search it never ran — which shell guards this run actually drove. Written
|
|
201
|
+
* only on request, and never committed: a checked-in trace would report a guard
|
|
202
|
+
* as evidenced long after its driving test was deleted, which is the same false
|
|
203
|
+
* claim with a longer half-life.
|
|
204
|
+
*
|
|
205
|
+
* A write that fails is announced and otherwise ignored. This check's verdict
|
|
206
|
+
* does not depend on the copy, and refusing the run because a convenience file
|
|
207
|
+
* could not be written would trade a real signal for a bookkeeping one.
|
|
208
|
+
* @param {string} root - Repository root.
|
|
209
|
+
* @param {readonly string[]} args - CLI arguments.
|
|
210
|
+
* @param {string} trace - The raw JSONL this run produced.
|
|
211
|
+
* @returns {void}
|
|
212
|
+
*/
|
|
213
|
+
function keepTrace(root, args, trace) {
|
|
214
|
+
const at = args.indexOf(TRACE_OUT_FLAG);
|
|
215
|
+
const target = at === -1 ? undefined : args[at + 1];
|
|
216
|
+
if (target === undefined) return;
|
|
217
|
+
const resolved = path.isAbsolute(target) ? target : path.join(root, target);
|
|
218
|
+
try {
|
|
219
|
+
mkdirSync(path.dirname(resolved), { recursive: true });
|
|
220
|
+
writeFileSync(resolved, trace);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error(
|
|
223
|
+
`check:shell-guard-refusal-coverage: could not write ${target}: ${
|
|
224
|
+
error instanceof Error ? error.message : String(error)
|
|
225
|
+
}`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
188
230
|
/**
|
|
189
231
|
* CLI entry point.
|
|
190
232
|
* @returns {void}
|
|
191
233
|
*/
|
|
192
234
|
export function main() {
|
|
193
235
|
const args = process.argv.slice(2);
|
|
194
|
-
const known = new Set(["--json", "--trace"]);
|
|
236
|
+
const known = new Set(["--json", "--trace", TRACE_OUT_FLAG]);
|
|
237
|
+
const valueFlags = new Set(["--trace", TRACE_OUT_FLAG]);
|
|
195
238
|
const unknown = args.find(
|
|
196
239
|
(arg, at) =>
|
|
197
|
-
arg.startsWith("--") &&
|
|
240
|
+
arg.startsWith("--") &&
|
|
241
|
+
!known.has(arg) &&
|
|
242
|
+
!valueFlags.has(args[at - 1] ?? "")
|
|
198
243
|
);
|
|
199
244
|
if (unknown) {
|
|
200
245
|
console.error(
|
|
@@ -216,6 +261,7 @@ export function main() {
|
|
|
216
261
|
process.exitCode = 2;
|
|
217
262
|
return;
|
|
218
263
|
}
|
|
264
|
+
keepTrace(root, args, produced.trace);
|
|
219
265
|
const report = judge({ population, observed: parseTrace(produced.trace) });
|
|
220
266
|
console.log(
|
|
221
267
|
args.includes("--json")
|
|
@@ -55,6 +55,14 @@
|
|
|
55
55
|
* It NEVER throws and never changes a return value: a tracer that can break the
|
|
56
56
|
* suite it observes would be turned off, and then this whole control is prose.
|
|
57
57
|
*
|
|
58
|
+
* Each record carries the `sha256` that matched, not only the guard path. A
|
|
59
|
+
* consumer reading a trace some other step produced cannot otherwise tell an
|
|
60
|
+
* observation of THIS guard from an observation of the version it replaced, and
|
|
61
|
+
* a guard that inherits its predecessor's coverage is the stale-evidence shape
|
|
62
|
+
* this repository keeps closing (CodySwannGT/lisa#3931). The judge in
|
|
63
|
+
* `shell-guard-refusal-coverage.mjs` does not need it — it hashes the tree
|
|
64
|
+
* itself in the same process — so the field is additive and ignored there.
|
|
65
|
+
*
|
|
58
66
|
* @module scripts/lib/shell-guard-trace
|
|
59
67
|
*/
|
|
60
68
|
import { appendFileSync, readFileSync, statSync } from "node:fs";
|
|
@@ -98,12 +106,15 @@ function install(trace, index) {
|
|
|
98
106
|
* @param {string} origin - Test frame that started it.
|
|
99
107
|
* @returns {void}
|
|
100
108
|
*/
|
|
101
|
-
const record = (script, status, origin) => {
|
|
109
|
+
const record = (script, status, origin, sha256) => {
|
|
102
110
|
const key = `${script}:${String(status)}`;
|
|
103
111
|
if (seen.has(key)) return;
|
|
104
112
|
seen.add(key);
|
|
105
113
|
try {
|
|
106
|
-
appendFileSync(
|
|
114
|
+
appendFileSync(
|
|
115
|
+
trace,
|
|
116
|
+
`${JSON.stringify({ script, status, origin, sha256 })}\n`
|
|
117
|
+
);
|
|
107
118
|
} catch {
|
|
108
119
|
/* A trace that cannot be written must not fail the suite. */
|
|
109
120
|
}
|
|
@@ -138,7 +149,7 @@ function install(trace, index) {
|
|
|
138
149
|
* @param {unknown} command - First argument of the child start.
|
|
139
150
|
* @param {unknown} args - Second argument: an argv array, or the options.
|
|
140
151
|
* @param {unknown} third - Third argument, when the second was an argv array.
|
|
141
|
-
* @returns {string[]}
|
|
152
|
+
* @returns {{guard: string, sha256: string}[]} Guards, each with the digest that matched.
|
|
142
153
|
*/
|
|
143
154
|
const matched = (command, args, third) => {
|
|
144
155
|
if (typeof command !== "string") return [];
|
|
@@ -157,7 +168,8 @@ function install(trace, index) {
|
|
|
157
168
|
typeof (/** @type {{cwd?: unknown}} */ (options).cwd) === "string"
|
|
158
169
|
? /** @type {{cwd: string}} */ (options).cwd
|
|
159
170
|
: process.cwd();
|
|
160
|
-
|
|
171
|
+
/** @type {Map<string, string>} */
|
|
172
|
+
const hits = new Map();
|
|
161
173
|
for (const token of tokens) {
|
|
162
174
|
if (!token || token.length > MAX_PATH_LENGTH) continue;
|
|
163
175
|
const absolute = path.isAbsolute(token)
|
|
@@ -179,12 +191,14 @@ function install(trace, index) {
|
|
|
179
191
|
const digest = createHash("sha256")
|
|
180
192
|
.update(readFileSync(absolute))
|
|
181
193
|
.digest("hex");
|
|
182
|
-
for (const guard of inventory.byHash[digest] ?? [])
|
|
194
|
+
for (const guard of inventory.byHash[digest] ?? []) {
|
|
195
|
+
hits.set(guard, digest);
|
|
196
|
+
}
|
|
183
197
|
} catch {
|
|
184
198
|
continue;
|
|
185
199
|
}
|
|
186
200
|
}
|
|
187
|
-
return [...hits];
|
|
201
|
+
return [...hits].map(([guard, sha256]) => ({ guard, sha256 }));
|
|
188
202
|
};
|
|
189
203
|
|
|
190
204
|
/**
|
|
@@ -213,7 +227,9 @@ function install(trace, index) {
|
|
|
213
227
|
const guards = matched(command, args, options);
|
|
214
228
|
if (guards.length > 0) {
|
|
215
229
|
const where = origin();
|
|
216
|
-
for (const
|
|
230
|
+
for (const hit of guards) {
|
|
231
|
+
record(hit.guard, result.status, where, hit.sha256);
|
|
232
|
+
}
|
|
217
233
|
}
|
|
218
234
|
} catch {
|
|
219
235
|
/* Never let observation change the observed. */
|
|
@@ -234,7 +250,9 @@ function install(trace, index) {
|
|
|
234
250
|
const guards = matched(command, args, options);
|
|
235
251
|
if (guards.length === 0) return;
|
|
236
252
|
const where = origin();
|
|
237
|
-
for (const
|
|
253
|
+
for (const hit of guards) {
|
|
254
|
+
record(hit.guard, status, where, hit.sha256);
|
|
255
|
+
}
|
|
238
256
|
} catch {
|
|
239
257
|
/* Never let observation change the observed. */
|
|
240
258
|
}
|
|
@@ -480,6 +480,25 @@ notice_marker=""
|
|
|
480
480
|
# non-sticky shared TMPDIR: another user could replace that parent between the
|
|
481
481
|
# checks below. Root- or caller-owned parents are acceptable; any parent that
|
|
482
482
|
# grants group or other write access must also carry the sticky bit.
|
|
483
|
+
#
|
|
484
|
+
# The two `stat` spellings must be asked for the SAME twelve bits, and getting
|
|
485
|
+
# that wrong is invisible because each platform only ever runs its own branch.
|
|
486
|
+
# BSD `%Lp` renders the low NINE bits and drops setuid/setgid/sticky entirely:
|
|
487
|
+
#
|
|
488
|
+
# stat -f '%Lp' <a 1777 dir> -> 777 <- sticky gone
|
|
489
|
+
# stat -f '%p' <a 1777 dir> -> 41777 <- sticky present, plus file type
|
|
490
|
+
# stat -c '%a' <a 1777 dir> -> 1777 <- sticky present (GNU includes it)
|
|
491
|
+
#
|
|
492
|
+
# So under `%Lp` the sticky test below is `777 & 1000`, which is zero for every
|
|
493
|
+
# directory on the machine — the exemption was unreachable on macOS and the
|
|
494
|
+
# whole `&& [ ... 8#1000 ] -eq 0` clause was dead code there. The same logical
|
|
495
|
+
# root was therefore trusted on Linux and rejected on macOS, and since CI is
|
|
496
|
+
# Linux the stricter platform was the one nobody specified and nobody tests on
|
|
497
|
+
# (CodySwannGT/lisa#3691).
|
|
498
|
+
#
|
|
499
|
+
# `%p` restores the bit and adds the file type above it, so the mask normalizes
|
|
500
|
+
# both spellings to one value and the arithmetic below has a single meaning:
|
|
501
|
+
# `41777 & 7777` and `1777 & 7777` are both 1777.
|
|
483
502
|
notice_directory_trusted() {
|
|
484
503
|
local directory="$1"
|
|
485
504
|
local directory_stat=""
|
|
@@ -487,7 +506,7 @@ notice_directory_trusted() {
|
|
|
487
506
|
local directory_mode=""
|
|
488
507
|
local directory_mode_value=0
|
|
489
508
|
|
|
490
|
-
directory_stat="$(stat -f '%u %
|
|
509
|
+
directory_stat="$(stat -f '%u %p' "$directory" 2>/dev/null)" || \
|
|
491
510
|
directory_stat="$(stat -c '%u %a' "$directory" 2>/dev/null)" || return 1
|
|
492
511
|
directory_owner="${directory_stat%% *}"
|
|
493
512
|
directory_mode="${directory_stat#* }"
|
|
@@ -495,7 +514,7 @@ notice_directory_trusted() {
|
|
|
495
514
|
*[!0-9:]* | :* | *: ) return 1 ;;
|
|
496
515
|
esac
|
|
497
516
|
[ "$directory_owner" = "0" ] || [ "$directory_owner" = "$notice_uid" ] || return 1
|
|
498
|
-
directory_mode_value=$((8#$directory_mode))
|
|
517
|
+
directory_mode_value=$((8#$directory_mode & 8#7777))
|
|
499
518
|
if [ $((directory_mode_value & 8#0022)) -ne 0 ] && \
|
|
500
519
|
[ $((directory_mode_value & 8#1000)) -eq 0 ]; then
|
|
501
520
|
return 1
|