@ai-ecoverse/slicc-kernel 1.176.1 → 1.177.0

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.
@@ -2796,8 +2796,8 @@ var OpfsFs = class {
2796
2796
  const [entry] = await this.meta.get([target]);
2797
2797
  if (entry?.fifo || entry?.socket) throw fsError("ENXIO", path);
2798
2798
  const kept = fresh3 ? cached : this.files.get(target);
2799
- const usable2 = kept && performance.now() - (this.found.get(kept) ?? -CHECK_MS) < CHECK_MS;
2800
- return [usable2 ? kept : void 0, target, true];
2799
+ const usable3 = kept && performance.now() - (this.found.get(kept) ?? -CHECK_MS) < CHECK_MS;
2800
+ return [usable3 ? kept : void 0, target, true];
2801
2801
  }
2802
2802
  async writeAt(handle, target, path, offset, bytes) {
2803
2803
  await this.counting(target);
@@ -5580,14 +5580,21 @@ async function ownerOnly(g, op, path, follow2) {
5580
5580
  return walked;
5581
5581
  }
5582
5582
  async function chmodMode(g, path, mode) {
5583
- const perm = (await ownerOnly(g, "chmod", path, true)).perm;
5584
- if (!g.dac.enforcing || !perm || g.cred.euid === 0 || inGroup(g.cred, perm.gid)) return mode;
5583
+ const walked = await need(g, "chmod", path, 0, { follow: true });
5584
+ const perm = walked.perm;
5585
+ const st = await g.fs.stat(walked.path).catch(() => void 0);
5586
+ if (!st?.fat && perm && g.cred.euid !== 0 && perm.uid !== g.cred.euid) {
5587
+ if (await present(g.fs, walked.path, false)) deny(g, "chmod", path, "EPERM");
5588
+ }
5589
+ if (!g.dac.enforcing || !perm || g.cred.euid === 0 || st?.fat || inGroup(g.cred, perm.gid)) {
5590
+ return mode;
5591
+ }
5585
5592
  return mode & ~S_ISGID3;
5586
5593
  }
5587
5594
  function heldChmodMode(g, path, st, mode) {
5588
5595
  const perm = permOf(st);
5589
- if (g.cred.euid !== 0 && perm.uid !== g.cred.euid) deny(g, "fchmod", path, "EPERM");
5590
- if (!g.dac.enforcing || g.cred.euid === 0 || inGroup(g.cred, perm.gid)) return mode;
5596
+ if (!st.fat && g.cred.euid !== 0 && perm.uid !== g.cred.euid) deny(g, "fchmod", path, "EPERM");
5597
+ if (!g.dac.enforcing || g.cred.euid === 0 || st.fat || inGroup(g.cred, perm.gid)) return mode;
5591
5598
  return mode & ~S_ISGID3;
5592
5599
  }
5593
5600
  function heldRetimable(g, path, st, now) {
@@ -8767,7 +8774,7 @@ function satisfies(version, range2) {
8767
8774
  }
8768
8775
 
8769
8776
  // src/version.ts
8770
- var KERNEL_VERSION = true ? "1.176.1" : "0.0.0-dev";
8777
+ var KERNEL_VERSION = true ? "1.177.0" : "0.0.0-dev";
8771
8778
  var OS_RELEASE = "6.0.0-slicc";
8772
8779
  function procVersion() {
8773
8780
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -13912,10 +13919,15 @@ var DriverConnection = class _DriverConnection {
13912
13919
  // src/mount/mount-fs.ts
13913
13920
  var S_ISGID4 = 1024;
13914
13921
  var ID = /^\d{1,10}$/;
13922
+ var MASK = /^0*[0-7]{1,4}$/;
13915
13923
  var MAX_ID2 = 4294967294;
13916
13924
  function idOption(value2) {
13917
13925
  return value2 === void 0 ? 0 : Number(value2);
13918
13926
  }
13927
+ function maskOption(value2) {
13928
+ if (value2 === void 0) return void 0;
13929
+ return Number.parseInt(value2, 8) & 511;
13930
+ }
13919
13931
  function checkIds(options) {
13920
13932
  for (const key of ["uid", "gid"]) {
13921
13933
  const value2 = options[key];
@@ -13923,6 +13935,27 @@ function checkIds(options) {
13923
13935
  if (!ID.test(value2) || Number(value2) > MAX_ID2) throw errnoError("EINVAL", `${key}=${value2}`);
13924
13936
  }
13925
13937
  }
13938
+ function checkMasks(options) {
13939
+ for (const key of ["umask", "fmask", "dmask"]) {
13940
+ const value2 = options[key];
13941
+ if (value2 === void 0) continue;
13942
+ if (!MASK.test(value2) || Number.parseInt(value2, 8) > 511) {
13943
+ throw errnoError("EINVAL", `${key}=${value2}`);
13944
+ }
13945
+ }
13946
+ }
13947
+ function modeOf(attr2, mount) {
13948
+ const type = attr2.kind === "directory" ? 16384 : attr2.kind === "symlink" ? 40960 : attr2.kind === "fifo" ? 4096 : 32768;
13949
+ const fallback = type | (attr2.kind === "directory" ? 493 : attr2.kind === "symlink" ? 511 : 420);
13950
+ if (mount.caps.owners) return attr2.mode ?? fallback;
13951
+ const umask = maskOption(mount.options.umask);
13952
+ const fmask = maskOption(mount.options.fmask) ?? umask;
13953
+ const dmask = maskOption(mount.options.dmask) ?? umask;
13954
+ const mask = attr2.kind === "directory" ? dmask : fmask;
13955
+ if (mask === void 0) return attr2.mode ?? fallback;
13956
+ const base = attr2.kind === "directory" || attr2.kind === "symlink" ? 511 : 438;
13957
+ return type | base & ~mask;
13958
+ }
13926
13959
  var MAX_LINKS7 = 40;
13927
13960
  var STALE_RETRIES = 3;
13928
13961
  var SETTLE_ROUNDS = 4;
@@ -14189,14 +14222,13 @@ function inodeless(mount, attr2) {
14189
14222
  }
14190
14223
  function statOf2(attr2, path, mount) {
14191
14224
  const mtime = new Date(attr2.mtime);
14192
- const fallback = attr2.kind === "directory" ? 16877 : attr2.kind === "symlink" ? 41471 : attr2.kind === "fifo" ? 4516 : 33188;
14193
14225
  return {
14194
14226
  isFile: attr2.kind === "file",
14195
14227
  isDirectory: attr2.kind === "directory",
14196
14228
  isSymbolicLink: attr2.kind === "symlink",
14197
14229
  ...attr2.kind === "fifo" ? { isFifo: true } : {},
14198
14230
  size: attr2.size,
14199
- mode: attr2.mode ?? fallback,
14231
+ mode: modeOf(attr2, mount),
14200
14232
  mtime,
14201
14233
  atime: attr2.atime === void 0 ? mtime : new Date(attr2.atime),
14202
14234
  ctime: mtime,
@@ -14210,7 +14242,8 @@ function statOf2(attr2, path, mount) {
14210
14242
  ...attr2.etag !== void 0 ? { version: attr2.etag } : {},
14211
14243
  ...mount.owners ? {
14212
14244
  uid: (mount.caps.owners ? attr2.uid : void 0) ?? idOption(mount.options.uid),
14213
- gid: (mount.caps.owners ? attr2.gid : void 0) ?? idOption(mount.options.gid)
14245
+ gid: (mount.caps.owners ? attr2.gid : void 0) ?? idOption(mount.options.gid),
14246
+ ...!mount.caps.owners && !mount.caps.chmod && (mount.options.umask !== void 0 || mount.options.fmask !== void 0 || mount.options.dmask !== void 0) ? { fat: true } : {}
14214
14247
  } : {}
14215
14248
  };
14216
14249
  }
@@ -14317,6 +14350,7 @@ var MountTable = class {
14317
14350
  if (!st.isDirectory) throw errnoError("ENOTDIR", target);
14318
14351
  const options = { ...spec.options };
14319
14352
  if (this.deps.owners) checkIds(options);
14353
+ checkMasks(options);
14320
14354
  const maxFile = options.maxfile !== void 0 ? parseSize(options.maxfile) : void 0;
14321
14355
  const opened2 = await this.deps.open(spec.type, { ...spec, target, options });
14322
14356
  let mounted;
@@ -18767,8 +18801,8 @@ var SCRUB_IDLE_MS = 50;
18767
18801
  var SCRUB_NONBLOCK_MAX = 64 * 1024;
18768
18802
  var encoder5 = new TextEncoder();
18769
18803
  function scrubberOf(secrets, sameLength = false) {
18770
- const usable2 = secrets.filter((s) => encoder5.encode(s.value).length >= MIN_VALUE);
18771
- return usable2.length > 0 ? compile(usable2, { idleMs: SCRUB_IDLE_MS, sameLength }) : void 0;
18804
+ const usable3 = secrets.filter((s) => encoder5.encode(s.value).length >= MIN_VALUE);
18805
+ return usable3.length > 0 ? compile(usable3, { idleMs: SCRUB_IDLE_MS, sameLength }) : void 0;
18772
18806
  }
18773
18807
  async function writeAll2(file, bytes) {
18774
18808
  const write = file.write;
@@ -19407,6 +19441,56 @@ var SealedBroker = class {
19407
19441
  }
19408
19442
  };
19409
19443
 
19444
+ // src/sudo-timestamp.ts
19445
+ var TIMESTAMPS_MAX = 64;
19446
+ var MINUTE_MS2 = 6e4;
19447
+ function timestampKey(owner, type) {
19448
+ if (type === "global") return "global";
19449
+ const { terminal, parent } = owner;
19450
+ if (type === "tty" && terminal) {
19451
+ return JSON.stringify(["tty", terminal.tty, terminal.sid, terminal.started]);
19452
+ }
19453
+ return parent ? JSON.stringify(["ppid", parent.pid, parent.started]) : void 0;
19454
+ }
19455
+ var Timestamps = class {
19456
+ users = /* @__PURE__ */ new Map();
19457
+ now;
19458
+ constructor(now) {
19459
+ this.now = now;
19460
+ }
19461
+ current(owner, policy) {
19462
+ if (policy.minutes === 0) return false;
19463
+ const key = timestampKey(owner, policy.type);
19464
+ const at = key === void 0 ? void 0 : this.users.get(owner.uid)?.get(key);
19465
+ if (at === void 0) return false;
19466
+ const now = this.now();
19467
+ return at <= now && (policy.minutes < 0 || now - at < policy.minutes * MINUTE_MS2);
19468
+ }
19469
+ stamp(owner, policy) {
19470
+ const key = policy.minutes === 0 ? void 0 : timestampKey(owner, policy.type);
19471
+ if (key === void 0) return false;
19472
+ const now = this.now();
19473
+ const records = this.users.get(owner.uid) ?? /* @__PURE__ */ new Map();
19474
+ this.users.set(owner.uid, records);
19475
+ records.delete(key);
19476
+ records.set(key, now);
19477
+ for (const [old, at] of records) {
19478
+ const stale = policy.minutes > 0 && (at > now || now - at >= policy.minutes * MINUTE_MS2);
19479
+ if (stale || records.size > TIMESTAMPS_MAX) records.delete(old);
19480
+ }
19481
+ return true;
19482
+ }
19483
+ invalidate(owner, type) {
19484
+ const key = timestampKey(owner, type);
19485
+ const records = this.users.get(owner.uid);
19486
+ if (key !== void 0) records?.delete(key);
19487
+ if (records?.size === 0) this.users.delete(owner.uid);
19488
+ }
19489
+ remove(uid) {
19490
+ this.users.delete(uid);
19491
+ }
19492
+ };
19493
+
19410
19494
  // src/sudoers.ts
19411
19495
  var SUDOERS = "/etc/sudoers";
19412
19496
  var SUDOERS_D = "/etc/sudoers.d";
@@ -19426,6 +19510,8 @@ var SHOWN_MAX = 60;
19426
19510
  var KERNEL_SYNTAX = "[NOPASSWD] <user|%group|ALL> [(<target>)] [Cwd <dir>] [Stdin none] Cmnd <command\u2026|*>";
19427
19511
  var SUDOERS_SYNTAX = "<user|%group|ALL> ALL=[(<target>[,<target>\u2026])] [NOPASSWD:] <command\u2026|ALL>";
19428
19512
  var EXPECTED = `expected '${KERNEL_SYNTAX}' or '${SUDOERS_SYNTAX}'`;
19513
+ var TIMESTAMP_TIMEOUT = 5;
19514
+ var TIMESTAMP_TYPE = "tty";
19429
19515
  function hasControl(text) {
19430
19516
  return CONTROL.test(text);
19431
19517
  }
@@ -19708,9 +19794,6 @@ function directiveError(text) {
19708
19794
  return /^#-?\d+\s+\S+\s*=/.test(text) ? "a #uid isn't supported: name the user, %group or ALL" : null;
19709
19795
  }
19710
19796
  const first = /^\S+/.exec(text)?.[0] ?? "";
19711
- if (/^Defaults([:@>!]|$)/.test(first)) {
19712
- return "Defaults aren't supported: the kernel's sudo has no settings, so this changes nothing";
19713
- }
19714
19797
  if (/^[A-Z][A-Za-z]*_Alias$/.test(first)) {
19715
19798
  return "aliases aren't supported: name the users, targets and commands in each line";
19716
19799
  }
@@ -19736,9 +19819,90 @@ function kernelLine(m, line, file) {
19736
19819
  )
19737
19820
  };
19738
19821
  }
19822
+ var DEFAULTS = /^Defaults(?=[:@>!\s]|$)/;
19823
+ var DEFAULTS_ONLY = `Defaults aren't supported, except timestamp_timeout and timestamp_type for everyone, a user or a %group: this line changes nothing`;
19824
+ var PARAM = /^(!*)([A-Za-z_]+)(?:\s*([-+]?=)\s*(.*))?$/s;
19825
+ var MINUTES = /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)$/;
19826
+ var TYPES = ["tty", "ppid", "global"];
19827
+ function defaultsValue(raw2) {
19828
+ const quoted = /^"(.*)"$/s.exec(raw2);
19829
+ return quoted ? quoted[1] : raw2;
19830
+ }
19831
+ function timeoutParam({ text, negated, op, value: value2 }) {
19832
+ if (negated && op === void 0) return { timestampTimeout: 0 };
19833
+ if (negated || op !== "=" || !MINUTES.test(value2) || !Number.isFinite(Number(value2))) {
19834
+ return `timestamp_timeout takes minutes, such as =5 or =2.5 (0 always asks, below 0 never expires), not ${shown(text)}`;
19835
+ }
19836
+ return { timestampTimeout: Number(value2) };
19837
+ }
19838
+ function typeParam({ text, negated, op, value: value2 }) {
19839
+ if (!negated && op === "=" && TYPES.includes(value2)) {
19840
+ return { timestampType: value2 };
19841
+ }
19842
+ if (value2 === "kernel") return "timestamp_type=kernel isn't supported: use tty, ppid or global";
19843
+ return `timestamp_type takes =tty, =ppid or =global, not ${shown(text)}`;
19844
+ }
19845
+ function defaultsParam(param) {
19846
+ const m = PARAM.exec(param);
19847
+ if (!m) return DEFAULTS_ONLY;
19848
+ const [, bangs = "", name2, op, raw2 = ""] = m;
19849
+ const read = { text: param, negated: bangs !== "", op, value: defaultsValue(raw2.trim()) };
19850
+ const settings = name2 === "timestamp_timeout" ? timeoutParam(read) : name2 === "timestamp_type" ? typeParam(read) : DEFAULTS_ONLY;
19851
+ if (typeof settings === "string") return settings;
19852
+ const value2 = settings.timestampTimeout ?? settings.timestampType;
19853
+ return { settings, text: `${name2}=${value2}` };
19854
+ }
19855
+ function defaultsWho(binding) {
19856
+ if (binding === "") return null;
19857
+ if (!binding.startsWith(":")) return DEFAULTS_ONLY;
19858
+ const who = binding.slice(1).split(",");
19859
+ for (const name2 of who) {
19860
+ if (!validWho(name2))
19861
+ return `expected users, %groups or ALL after Defaults:, not ${shown(name2)}`;
19862
+ }
19863
+ return who;
19864
+ }
19865
+ function defaultsLine(text) {
19866
+ const m = /^Defaults(\S*)(?:\s+(.*))?$/s.exec(text);
19867
+ const who = defaultsWho(m?.[1] ?? "");
19868
+ if (typeof who === "string") return { error: who };
19869
+ const list = (m?.[2] ?? "").trim();
19870
+ if (list === "")
19871
+ return {
19872
+ error: "expected timestamp_timeout=<minutes> or timestamp_type=<type> after Defaults"
19873
+ };
19874
+ const settings = {};
19875
+ const params = [];
19876
+ for (const param of list.split(",")) {
19877
+ const read = defaultsParam(param.trim());
19878
+ if (typeof read === "string") return { error: read };
19879
+ Object.assign(settings, read.settings);
19880
+ params.push(read.text);
19881
+ }
19882
+ return { defaults: { who, settings, params } };
19883
+ }
19884
+ function resolveDefaults(entries, applies) {
19885
+ const everyone = entries.filter((e) => e.who === null);
19886
+ const mine = entries.filter((e) => e.who?.some(applies));
19887
+ const matching = [...everyone, ...mine];
19888
+ const settings = Object.assign({}, ...matching.map((e) => e.settings));
19889
+ return {
19890
+ timestampTimeout: settings.timestampTimeout ?? TIMESTAMP_TIMEOUT,
19891
+ timestampType: settings.timestampType ?? TIMESTAMP_TYPE,
19892
+ matching
19893
+ };
19894
+ }
19895
+ function defaultsText(entries) {
19896
+ const all = resolveDefaults(entries, () => false);
19897
+ return [
19898
+ `Defaults timestamp_timeout=${all.timestampTimeout}, timestamp_type=${all.timestampType}`,
19899
+ ...entries.filter((e) => e.who !== null).map((e) => `Defaults:${e.who.join(",")} ${e.params.join(", ")}`)
19900
+ ];
19901
+ }
19739
19902
  function parseLine(line, file) {
19740
19903
  const text = line.trim();
19741
19904
  if (text === "") return null;
19905
+ if (DEFAULTS.test(text)) return defaultsLine(text);
19742
19906
  const directive = directiveError(text);
19743
19907
  if (directive !== void 0) return directive === null ? null : { error: directive };
19744
19908
  const kernel = KERNEL_FORM.exec(text);
@@ -19750,14 +19914,16 @@ function parseLine(line, file) {
19750
19914
  }
19751
19915
  function parsePolicy(text, file) {
19752
19916
  const rules = [];
19917
+ const defaults2 = [];
19753
19918
  const errors = [];
19754
19919
  for (const [n, line] of text.split("\n").entries()) {
19755
19920
  const parsed2 = parseLine(line, file);
19756
19921
  if (parsed2 === null) continue;
19757
19922
  if ("error" in parsed2) errors.push({ line: n + 1, message: parsed2.error });
19923
+ else if ("defaults" in parsed2) defaults2.push(parsed2.defaults);
19758
19924
  else rules.push(...parsed2.rules);
19759
19925
  }
19760
- return { rules, errors };
19926
+ return { rules, defaults: defaults2, errors };
19761
19927
  }
19762
19928
 
19763
19929
  // src/sudo.ts
@@ -19765,7 +19931,14 @@ var SECURE_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
19765
19931
  var DEFAULT_SUDOERS = "%sudo Cmnd *\n";
19766
19932
  var SUDO_TIMEOUT_MS = 3e5;
19767
19933
  var TERM = /^[A-Za-z0-9._+-]+$/;
19768
- var USAGE = "usage: sudo [-n] [-u user] [--] command [args...]\n sudo -l";
19934
+ var USAGE = [
19935
+ "usage: sudo -K | -k",
19936
+ " sudo -v [-kn] [-u user]",
19937
+ " sudo -l [-kn]",
19938
+ " sudo [-kn] [-u user] [--] command [args...]"
19939
+ ].join("\n");
19940
+ var DONE = { done: true };
19941
+ var NOT_STAMPED = "sudo: approved, but the approver didn't say a person decided, so no timestamp was recorded";
19769
19942
  var DENIED2 = "sudo: approval denied";
19770
19943
  var DENIAL_MAX = 500;
19771
19944
  var ABORTED = /* @__PURE__ */ Symbol("aborted");
@@ -19839,6 +20012,17 @@ async function answerOf(approve, req, signal) {
19839
20012
  return cut === void 0 ? "deny" : await cut;
19840
20013
  }
19841
20014
  }
20015
+ function approvalOf(answer2) {
20016
+ if (answer2 === "once" || answer2 === "always") return { answer: answer2, person: false };
20017
+ if (typeof answer2 !== "object" || answer2 === null) return void 0;
20018
+ try {
20019
+ const { answer: kind, timestamp } = answer2;
20020
+ if (kind !== "once" && kind !== "always" || timestamp !== true) return void 0;
20021
+ return { answer: kind, person: true };
20022
+ } catch {
20023
+ return void 0;
20024
+ }
20025
+ }
19842
20026
  function recordedCommand(argv) {
19843
20027
  const [path = "", ...args] = argv;
19844
20028
  return commandText({ path, args: args.map((a) => [a]) });
@@ -19848,6 +20032,9 @@ function flags(parsed2, arg, next3) {
19848
20032
  const flag = arg[j];
19849
20033
  if (flag === "n") parsed2.nonInteractive = true;
19850
20034
  else if (flag === "l") parsed2.list = true;
20035
+ else if (flag === "v") parsed2.validate = true;
20036
+ else if (flag === "k") parsed2.reset = true;
20037
+ else if (flag === "K") parsed2.removeAll = true;
19851
20038
  else if (flag !== "u") return `sudo: invalid option -- '${flag}'
19852
20039
  ${USAGE}`;
19853
20040
  else {
@@ -19861,7 +20048,15 @@ ${USAGE}`;
19861
20048
  return 0;
19862
20049
  }
19863
20050
  function parseArgs(args) {
19864
- const parsed2 = { list: false, nonInteractive: false, user: "root", command: [] };
20051
+ const parsed2 = {
20052
+ list: false,
20053
+ nonInteractive: false,
20054
+ validate: false,
20055
+ reset: false,
20056
+ removeAll: false,
20057
+ user: "root",
20058
+ command: []
20059
+ };
19865
20060
  let i = 0;
19866
20061
  for (; i < args.length; i++) {
19867
20062
  const arg = args[i];
@@ -19875,8 +20070,17 @@ function parseArgs(args) {
19875
20070
  i += used2;
19876
20071
  }
19877
20072
  parsed2.command = args.slice(i);
19878
- if (parsed2.list ? parsed2.command.length > 0 : parsed2.command.length === 0) return USAGE;
19879
- return parsed2;
20073
+ return usable(parsed2) ? parsed2 : USAGE;
20074
+ }
20075
+ function usable(parsed2) {
20076
+ const command = parsed2.command.length > 0;
20077
+ const modes = [parsed2.list, parsed2.validate, parsed2.removeAll].filter(Boolean).length;
20078
+ if (modes > 1 || parsed2.removeAll && parsed2.reset) return false;
20079
+ if (modes === 1) return !command;
20080
+ return command || parsed2.reset;
20081
+ }
20082
+ function policyOf(settings) {
20083
+ return { type: settings.timestampType, minutes: settings.timestampTimeout };
19880
20084
  }
19881
20085
  function described(rule) {
19882
20086
  const cmnd = rule.cmnd === "*" ? "ALL" : commandText(rule.cmnd);
@@ -19888,9 +20092,11 @@ var Sudo = class {
19888
20092
  o;
19889
20093
  warned = /* @__PURE__ */ new Set();
19890
20094
  pending = /* @__PURE__ */ new Set();
20095
+ timestamps;
19891
20096
  next = 0;
19892
20097
  constructor(options) {
19893
20098
  this.o = options;
20099
+ this.timestamps = new Timestamps(options.now ?? (() => performance.now()));
19894
20100
  }
19895
20101
  warn(message3) {
19896
20102
  if (this.warned.has(message3)) return;
@@ -19977,8 +20183,8 @@ var Sudo = class {
19977
20183
  const code = err.code;
19978
20184
  return skipped(`it can't be read (${typeof code === "string" ? code : "error"})`);
19979
20185
  }
19980
- const { rules, errors } = parsePolicy(text, path);
19981
- return { path, rules, problems: errors, ok: true };
20186
+ const { rules, defaults: defaults2, errors } = parsePolicy(text, path);
20187
+ return { path, rules, defaults: defaults2, problems: errors, ok: true };
19982
20188
  }
19983
20189
  async policy() {
19984
20190
  const files = [await this.policyFile(SUDOERS)];
@@ -20007,7 +20213,10 @@ var Sudo = class {
20007
20213
  }
20008
20214
  }
20009
20215
  if (!detailed && skipped > 0) await caller.report?.(skippedNotice(skipped));
20010
- return files.flatMap((f) => f.rules);
20216
+ return {
20217
+ rules: files.flatMap((f) => f.rules),
20218
+ defaults: files.flatMap((f) => f.defaults ?? [])
20219
+ };
20011
20220
  }
20012
20221
  async check(cred, call) {
20013
20222
  const lines2 = [];
@@ -20020,17 +20229,26 @@ var Sudo = class {
20020
20229
  if (read && problems.length === 0) lines2.push([1, `${path}: parsed OK`]);
20021
20230
  if (problems.length > 0) ok4 = false;
20022
20231
  };
20232
+ const values = (defaults2) => {
20233
+ for (const line of defaultsText(defaults2)) lines2.push([1, line]);
20234
+ };
20023
20235
  if (call.files !== void 0) {
20236
+ const defaults2 = [];
20024
20237
  for (const { name: name2, text } of call.files) {
20025
- report({ path: name2, rules: [], problems: parsePolicy(text).errors, ok: true });
20238
+ const parsed2 = parsePolicy(text);
20239
+ defaults2.push(...parsed2.defaults);
20240
+ report({ path: name2, rules: [], problems: parsed2.errors, ok: true });
20026
20241
  }
20242
+ if (defaults2.length > 0) values(defaults2);
20027
20243
  return { lines: lines2, ok: ok4 };
20028
20244
  }
20029
20245
  if (this.o.mode === "enforce" && cred.euid !== 0) {
20030
20246
  const hint = "check the policy as root (sudo visudo -c), or a file with visudo -c -f <file>";
20031
20247
  return { lines: [[2, `visudo: ${SUDOERS}: Permission denied: ${hint}`]], ok: false };
20032
20248
  }
20033
- for (const file of await this.policy()) report(file);
20249
+ const files = await this.policy();
20250
+ for (const file of files) report(file);
20251
+ values(files.flatMap((f) => f.defaults ?? []));
20034
20252
  return { lines: lines2, ok: ok4 };
20035
20253
  }
20036
20254
  async groupIds() {
@@ -20039,9 +20257,9 @@ var Sudo = class {
20039
20257
  async applies(caller, cred) {
20040
20258
  const gids = await this.groupIds();
20041
20259
  const mine = /* @__PURE__ */ new Set([cred.rgid, cred.egid, ...cred.groups]);
20042
- return (rule) => {
20043
- if (rule.who === "ALL" || rule.who === caller.name) return true;
20044
- const gid = rule.who.startsWith("%") ? gids.get(rule.who.slice(1)) : void 0;
20260
+ return (who) => {
20261
+ if (who === "ALL" || who === caller.name) return true;
20262
+ const gid = who.startsWith("%") ? gids.get(who.slice(1)) : void 0;
20045
20263
  return gid !== void 0 && mine.has(gid);
20046
20264
  };
20047
20265
  }
@@ -20086,18 +20304,40 @@ var Sudo = class {
20086
20304
  const mode = this.o.mode;
20087
20305
  if (parsed2.list) return { list: `sudo: users are '${mode}': commands run as the caller
20088
20306
  ` };
20089
- return { run: parsed2.command };
20307
+ return parsed2.command.length > 0 ? { run: parsed2.command } : DONE;
20090
20308
  }
20091
20309
  const { cred } = caller;
20092
20310
  const policy = await this.policyRules(caller);
20093
20311
  const self = await this.o.users.lookup(cred.ruid);
20094
20312
  if (!self) return failed2("EPERM", `sudo: uid ${cred.ruid} is not in the passwd database`);
20313
+ const owner = {
20314
+ uid: cred.ruid,
20315
+ terminal: caller.terminal,
20316
+ parent: caller.parent
20317
+ };
20318
+ const applies = await this.applies(self, cred);
20319
+ const settings = resolveDefaults(policy.defaults, applies);
20320
+ if (parsed2.removeAll) {
20321
+ this.timestamps.remove(owner.uid);
20322
+ return DONE;
20323
+ }
20324
+ if (parsed2.reset && !parsed2.validate && !parsed2.list && parsed2.command.length === 0) {
20325
+ this.timestamps.invalidate(owner, settings.timestampType);
20326
+ return DONE;
20327
+ }
20095
20328
  const target = await this.lookup(parsed2.user);
20096
20329
  if (!target) return failed2("ENOENT", `sudo: unknown user ${parsed2.user}`);
20097
20330
  const root = cred.euid === 0;
20098
- const rules = root ? [] : policy;
20099
- const applies = await this.applies(self, cred);
20100
- if (parsed2.list) return this.listed(self, root, rules.filter(applies));
20331
+ const rules = root ? [] : policy.rules;
20332
+ const mine = rules.filter((rule) => applies(rule.who));
20333
+ if (parsed2.list) return this.listed(self, root, mine, settings.matching);
20334
+ const session = { caller, self, target, owner, policy: policyOf(settings), parsed: parsed2, signal };
20335
+ if (parsed2.validate) return this.validate(session, root, mine, call.cwd);
20336
+ return this.command(session, root, mine, call);
20337
+ }
20338
+ async command(s, root, mine, call) {
20339
+ const { caller, self, target, parsed: parsed2 } = s;
20340
+ const { cred } = caller;
20101
20341
  const [word = "", ...args] = parsed2.command;
20102
20342
  const cwd = await this.canonical(call.cwd);
20103
20343
  if (cwd === void 0 || !await this.reachable(cred, cwd)) {
@@ -20111,8 +20351,8 @@ var Sudo = class {
20111
20351
  const untrusted2 = await this.untrusted(path, cwd, word);
20112
20352
  if (untrusted2) return untrusted2;
20113
20353
  const argv = [path, ...args];
20114
- const matching = rules.filter(
20115
- (rule) => applies(rule) && (rule.target === "ALL" || rule.target === target.name) && (rule.cwd === void 0 || rule.cwd === cwd) && stdinMatches(rule, caller.stdin) && commandMatches(rule.cmnd, argv)
20354
+ const matching = mine.filter(
20355
+ (rule) => (rule.target === "ALL" || rule.target === target.name) && (rule.cwd === void 0 || rule.cwd === cwd) && stdinMatches(rule, caller.stdin) && commandMatches(rule.cmnd, argv)
20116
20356
  );
20117
20357
  const grant = () => this.grant(self, target, cred, parsed2.command, path, cwd, call.term);
20118
20358
  if (root || matching.some((rule) => rule.nopasswd)) return grant();
@@ -20123,23 +20363,67 @@ var Sudo = class {
20123
20363
  `sudo: ${self.name} is not allowed to run ${quoted} as ${target.name}`
20124
20364
  );
20125
20365
  }
20366
+ if (!parsed2.reset && this.timestamps.current(s.owner, s.policy)) {
20367
+ this.timestamps.stamp(s.owner, s.policy);
20368
+ this.logged(s, cwd, argv);
20369
+ return grant();
20370
+ }
20126
20371
  if (parsed2.nonInteractive) return failed2("EPERM", "sudo: a password is required");
20127
- if (this.pending.has(cred.ruid)) return failed2("EBUSY", "sudo: another request is pending");
20128
- this.pending.add(cred.ruid);
20129
- try {
20130
- const answer2 = await this.ask(caller, self, target, parsed2.command, path, cwd, signal);
20131
- if (typeof answer2 !== "string") return answer2;
20372
+ return this.pendingFor(cred.ruid, async () => {
20373
+ const answer2 = await this.ask(s, { argv: parsed2.command, path, cwd });
20374
+ if ("error" in answer2) return answer2;
20132
20375
  if (await this.canonical(call.cwd) !== cwd || !await this.reachable(cred, cwd)) {
20133
20376
  return failed2("EACCES", `sudo: ${call.cwd} changed while the request waited`);
20134
20377
  }
20135
20378
  const changed = await this.untrusted(path, cwd, word);
20136
20379
  if (changed) return changed;
20137
- if (answer2 === "once") return grant();
20138
- return await this.always(self, target, cwd, argv, recordable(cwd, parsed2.command), grant);
20380
+ const result = answer2.answer === "once" ? grant() : await this.always(self, target, cwd, argv, recordable(cwd, parsed2.command), grant);
20381
+ if (!("error" in result)) this.stamped(s, answer2);
20382
+ return result;
20383
+ });
20384
+ }
20385
+ async validate(s, root, mine, dir) {
20386
+ const { self, parsed: parsed2 } = s;
20387
+ if (root) return DONE;
20388
+ if (mine.length === 0) {
20389
+ return failed2("EPERM", `Sorry, user ${self.name} may not run sudo on ${this.o.hostname}.`);
20390
+ }
20391
+ if (mine.every((rule) => rule.nopasswd)) return DONE;
20392
+ if (!parsed2.reset && this.timestamps.current(s.owner, s.policy)) {
20393
+ this.timestamps.stamp(s.owner, s.policy);
20394
+ return DONE;
20395
+ }
20396
+ if (parsed2.nonInteractive) return failed2("EPERM", "sudo: a password is required");
20397
+ const cwd = await this.canonical(dir) ?? normalizePath(dir);
20398
+ return this.pendingFor(s.caller.cred.ruid, async () => {
20399
+ const answer2 = await this.ask(s, { argv: [], path: "", cwd, validate: true });
20400
+ if ("error" in answer2) return answer2;
20401
+ if (!answer2.person) await s.caller.report?.(NOT_STAMPED);
20402
+ this.stamped(s, answer2);
20403
+ return DONE;
20404
+ });
20405
+ }
20406
+ async pendingFor(uid, work) {
20407
+ if (this.pending.has(uid)) return failed2("EBUSY", "sudo: another request is pending");
20408
+ this.pending.add(uid);
20409
+ try {
20410
+ return await work();
20139
20411
  } finally {
20140
- this.pending.delete(cred.ruid);
20412
+ this.pending.delete(uid);
20141
20413
  }
20142
20414
  }
20415
+ stamped(s, answer2) {
20416
+ if (answer2.person && !s.parsed.reset) this.timestamps.stamp(s.owner, s.policy);
20417
+ }
20418
+ logged(s, cwd, argv) {
20419
+ const { caller, self, target, owner, policy } = s;
20420
+ const type = policy.type === "tty" && !owner.terminal ? "ppid" : policy.type;
20421
+ const tty = owner.terminal?.tty ?? caller.tty ?? "unknown";
20422
+ const command = argv.map(quoteWord).join(" ");
20423
+ (this.o.warn ?? console.warn)(
20424
+ `sudo: ${self.name} : TTY=${tty} ; PWD=${quoteWord(cwd)} ; USER=${target.name} ; TIMESTAMP=${type} ; COMMAND=${command}`
20425
+ );
20426
+ }
20143
20427
  async always(self, target, cwd, argv, shown2, grant) {
20144
20428
  if (!shown2 || !recordable(cwd, argv)) return { ...grant(), notice: RAN_ONCE };
20145
20429
  if (!await this.remember(self, target, cwd, argv)) {
@@ -20147,7 +20431,7 @@ var Sudo = class {
20147
20431
  }
20148
20432
  return grant();
20149
20433
  }
20150
- listed(self, root, rules) {
20434
+ listed(self, root, rules, defaults2) {
20151
20435
  const host = this.o.hostname;
20152
20436
  if (root) {
20153
20437
  return {
@@ -20160,11 +20444,20 @@ var Sudo = class {
20160
20444
  return failed2("EPERM", `User ${self.name} is not allowed to run sudo on ${host}.`);
20161
20445
  }
20162
20446
  const lines2 = rules.map(described).join("\n");
20163
- return { list: `User ${self.name} may run the following commands on ${host}:
20447
+ const params = defaults2.flatMap((d) => d.params).join(", ");
20448
+ const matching = params === "" ? "" : `Matching Defaults entries for ${self.name} on ${host}:
20449
+ ${params}
20450
+
20451
+ `;
20452
+ return {
20453
+ list: `${matching}User ${self.name} may run the following commands on ${host}:
20164
20454
  ${lines2}
20165
- ` };
20455
+ `
20456
+ };
20166
20457
  }
20167
- async ask(caller, self, target, argv, path, cwd, requester) {
20458
+ async ask(s, asked) {
20459
+ const { caller, self, target, signal: requester } = s;
20460
+ const { argv, path, cwd } = asked;
20168
20461
  const { approve } = this.o;
20169
20462
  if (!approve || requester.aborted) return failed2("EACCES", DENIED2);
20170
20463
  const createdAt = Date.now();
@@ -20183,9 +20476,11 @@ ${lines2}
20183
20476
  ...caller.tty ? { tty: caller.tty } : {},
20184
20477
  createdAt,
20185
20478
  expiresAt: createdAt + timeout,
20186
- always: recordable(cwd, [path, ...argv]),
20479
+ always: asked.validate ? false : recordable(cwd, [path, ...argv]),
20187
20480
  stdin: caller.stdin,
20188
- rootOnly: await this.rootOnly(cwd, argv.slice(1))
20481
+ rootOnly: asked.validate ? {} : await this.rootOnly(cwd, argv.slice(1)),
20482
+ ...asked.validate ? { validate: true } : {},
20483
+ timestamp: true
20189
20484
  };
20190
20485
  const control = new AbortController();
20191
20486
  const timer = setTimeout(() => control.abort("expired"), timeout);
@@ -20195,9 +20490,8 @@ ${lines2}
20195
20490
  try {
20196
20491
  const answer2 = await answerOf(approve, req, control.signal);
20197
20492
  if (answer2 === ABORTED) return ended();
20198
- if (answer2 === "once" || answer2 === "always") {
20199
- return control.signal.aborted ? ended() : answer2;
20200
- }
20493
+ const approved = approvalOf(answer2);
20494
+ if (approved) return control.signal.aborted ? ended() : approved;
20201
20495
  return failed2("EACCES", denialText(answer2));
20202
20496
  } finally {
20203
20497
  clearTimeout(timer);
@@ -23914,7 +24208,7 @@ function replaced(text, values) {
23914
24208
  for (const [ph, value2] of Object.entries(values)) out = out.split(ph).join(binary(value2));
23915
24209
  return out;
23916
24210
  }
23917
- function usable(values) {
24211
+ function usable2(values) {
23918
24212
  const out = {};
23919
24213
  for (const [ph, value2] of Object.entries(values ?? {}))
23920
24214
  if (encoder8.encode(value2).length >= MIN_VALUE) out[ph] = value2;
@@ -23929,7 +24223,7 @@ async function substituteHead(head, ask) {
23929
24223
  if (!request3 || !found || found.length !== 1) return void 0;
23930
24224
  const at = found[0];
23931
24225
  if (at.placeholders.length > SESSION_PLACEHOLDERS) return void 0;
23932
- const values = usable(
24226
+ const values = usable2(
23933
24227
  await ask(`header:${at.name}`, at.placeholders, {
23934
24228
  method: request3[1],
23935
24229
  path: request3[2]
@@ -24239,7 +24533,7 @@ async function cleartext(message3, asks) {
24239
24533
  const placeholders = placeholdersIn(password);
24240
24534
  if (placeholders.length === 0 || placeholders.length > SESSION_PLACEHOLDERS)
24241
24535
  return { bytes: message3, done: true };
24242
- const values = usable(await asks.cleartext(placeholders));
24536
+ const values = usable2(await asks.cleartext(placeholders));
24243
24537
  if (Object.keys(values).length === 0) return { bytes: message3, done: true };
24244
24538
  return { bytes: frame3(112, `${replaced(password, values)}\0`), values, done: true };
24245
24539
  }
@@ -24304,7 +24598,7 @@ var PgClient = class {
24304
24598
  const x = got.exchange;
24305
24599
  const binding = this.header === END_POINT ? this.server.binding : void 0;
24306
24600
  const serverFinal = `c=${channelOf(this.header, binding)},r=${got.nonce}`;
24307
- const values = usable(
24601
+ const values = usable2(
24308
24602
  await this.asks.scram({
24309
24603
  clientFirstBare: x.clientFirstBare,
24310
24604
  serverFirst: x.serverFirst,
@@ -24353,7 +24647,7 @@ function decoded(b642) {
24353
24647
  async function swapped2(text, position, ask) {
24354
24648
  const placeholders = placeholdersIn(text);
24355
24649
  if (placeholders.length === 0 || placeholders.length > SESSION_PLACEHOLDERS) return void 0;
24356
- const values = usable(await ask(position, placeholders));
24650
+ const values = usable2(await ask(position, placeholders));
24357
24651
  if (Object.keys(values).length === 0) return void 0;
24358
24652
  return { text: replaced(text, values), values };
24359
24653
  }
@@ -24451,7 +24745,7 @@ function headEnd2(bytes) {
24451
24745
  }
24452
24746
  return -1;
24453
24747
  }
24454
- function modeOf(start2) {
24748
+ function modeOf2(start2) {
24455
24749
  const sniffs = [
24456
24750
  ["http1", sniffHttp1(start2)],
24457
24751
  ["smtp", sniffSmtp(start2)],
@@ -24560,7 +24854,7 @@ var SessionWriter = class {
24560
24854
  joined5.set(bytes, this.buffer.length);
24561
24855
  this.buffer.fill(0);
24562
24856
  this.buffer = joined5;
24563
- if (this.mode === "sniff") this.mode = modeOf(latin12(joined5.subarray(0, 8))) ?? "sniff";
24857
+ if (this.mode === "sniff") this.mode = modeOf2(latin12(joined5.subarray(0, 8))) ?? "sniff";
24564
24858
  if (this.mode === "pg" && !this.sawPg) {
24565
24859
  this.sawPg = true;
24566
24860
  if (!this.s.verified) void this.proof("pg");
@@ -33000,6 +33294,7 @@ var SUDO_SOURCE = [
33000
33294
  " await say(1, answer.list);",
33001
33295
  " return 0;",
33002
33296
  " }",
33297
+ " if (answer.done === true) return 0;",
33003
33298
  " if (answer.run !== undefined) {",
33004
33299
  " try {",
33005
33300
  " pid = (await ctx.spawn({ argv: answer.run, pgid: 0 })).pid;",
@@ -33639,6 +33934,7 @@ var Launcher = class {
33639
33934
  );
33640
33935
  }
33641
33936
  this.users = new UserDb(this.fs, this.owners);
33937
+ const { sudoClock } = options;
33642
33938
  this.sudoer = new Sudo({
33643
33939
  fs: this.fs,
33644
33940
  users: this.users,
@@ -33648,7 +33944,8 @@ var Launcher = class {
33648
33944
  pnpmHome: this.pnpmHome,
33649
33945
  backing: (path, cwd) => this.backing(path, cwd),
33650
33946
  approve: options.approve,
33651
- timeoutMs: options.sudoTimeoutMs
33947
+ timeoutMs: options.sudoTimeoutMs,
33948
+ now: sudoClock ? () => sudoClock.now() : void 0
33652
33949
  });
33653
33950
  this.cron = this.cronService(options.cron);
33654
33951
  void this.unlinked.sweep("/");
@@ -35054,11 +35351,26 @@ var Launcher = class {
35054
35351
  pid: this.jobs.shown(pid),
35055
35352
  tags: this.tagsOf(pid),
35056
35353
  tty: this.described.get(pid)?.tty,
35354
+ ...this.timestampOwner(pid),
35057
35355
  stdin,
35058
35356
  report
35059
35357
  };
35060
35358
  return this.sudoer.request(caller, call, signal);
35061
35359
  }
35360
+ timestampOwner(pid) {
35361
+ const sid = this.jobs.sidOf(pid);
35362
+ const tty = this.jobs.controllingTerminal(pid)?.name;
35363
+ const leader = sid === void 0 ? void 0 : this.startedOf(sid);
35364
+ const host = this.jobs.shownParent(pid) ?? this.jobs.shown(pid);
35365
+ const started = this.startedOf(host);
35366
+ return {
35367
+ ...tty && sid !== void 0 && leader !== void 0 ? { terminal: { tty, sid, started: leader } } : {},
35368
+ ...started === void 0 ? {} : { parent: { pid: host, started } }
35369
+ };
35370
+ }
35371
+ startedOf(shown2) {
35372
+ return this.running(shown2) ? this.described.get(shown2)?.started : void 0;
35373
+ }
35062
35374
  visudo(pid, call) {
35063
35375
  return this.sudoer.check(this.credOf(pid), call);
35064
35376
  }