let a member read the two directories above their own home
`bun run` from anywhere inside a member's home died with
error loading current directory
error: An internal error occurred (CouldntReadCurrentDirectory)
before it looked at package.json, bun.lock or .git — all of which were present.
it is not walking up looking for a workspace root. it primes its resolver cache
by walking DOWN from / and opening every component of the cwd for READING:
openat("/home/pastilhas/officerdev/") = 6
openat(".../officerdev/data/") = -1 EACCES
openat(".../officerdev/data/<email>/") = -1 EACCES
those two are 711 — traversable, not listable — which is enough to cd into a
home and not enough for a program that reads its ancestors. `getcwd` succeeds;
the ancestor read is what fails. `O_PATH` would need only `x`, so this is
arguably bun's bug, but it presents as a member's project being mysteriously
unbuildable and nothing here can fix it from the other side.
so DATA_PATH and the account dir now carry a named ACL entry per member. that
gives up the property the old comment named — a member can now `ls` DATA_PATH
and learn the other accounts' email addresses — and keeps everything that
matters: every home is still 700 and owned by its member, every platform
sibling still 700 and owned by the service user. verified as green: the account
list is visible, and email_accounts, another home, the repo .env, the owner's
ssh key, .pgpass and ~/.claude/.credentials.json are all still denied.
the mask is set explicitly to rx alongside the entry. chmod recomputes the mask
from the group bits, which for 711 is --x, so without that the next member's
provisioning would silently clamp every earlier member back to traverse-only.
applied by hand to the one existing account; provisioning covers new ones.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+37
-2
@@ -297,11 +297,46 @@ export async function confineUserTree(params: {
|
||||
try {
|
||||
if (!existsSync(home)) await mkdir(home, { recursive: true });
|
||||
|
||||
// Traversable, not listable. Applied to DATA_PATH itself too: without it a member can read the
|
||||
// directory and learn every other member's email address.
|
||||
// Traversable, not listable — for everyone EXCEPT the members themselves, who are named below.
|
||||
//
|
||||
// ── Why "not listable" could not be kept ──
|
||||
//
|
||||
// 711 says: pass through, do not read. That is enough to `cd` into a home and not enough for a program
|
||||
// that READS its ancestors, and at least one in daily use does. `bun run` primes its module-resolution
|
||||
// cache by walking DOWN from `/` and opening every component of the cwd with `O_RDONLY|O_DIRECTORY`:
|
||||
//
|
||||
// openat("/home/pastilhas/officerdev/") = 6
|
||||
// openat("/home/pastilhas/officerdev/data/") = -1 EACCES
|
||||
// openat("/home/pastilhas/officerdev/data/<email>/") = -1 EACCES
|
||||
//
|
||||
// and dies with `CouldntReadCurrentDirectory` before it ever looks for `package.json`. `getcwd` succeeds;
|
||||
// it is the read of the ancestors that fails. Traversal alone would do — `O_PATH` needs only `x` — so
|
||||
// this is arguably Bun's bug, but it is not one this repository can fix, and it presents as a project
|
||||
// being mysteriously unbuildable from a member's shell.
|
||||
//
|
||||
// The cost is stated plainly: a member can now `ls` DATA_PATH and learn the other accounts' email
|
||||
// addresses. Their CONTENTS stay shut — every `home` is 700 and owned by its member, and every sibling
|
||||
// is 700 and owned by the service user. What is given up is the account list, not any account's data.
|
||||
//
|
||||
// Named ACL entries rather than `chmod 755`, so this reaches members and not every account on the box.
|
||||
await chmod(DATA_PATH, 0o711);
|
||||
await chmod(accountDir, 0o711);
|
||||
|
||||
// After the chmods, never before — chmod recomputes the ACL mask from the group bits, which for 711 is
|
||||
// `--x`, and that would clamp every member entry (including ones added by earlier provisions) down to
|
||||
// traverse-only. Setting `m::rx` explicitly restores them all, so provisioning a second member does not
|
||||
// silently re-break the first.
|
||||
const uidEntry = `u:${params.uid}:rx`;
|
||||
const openUp = await run(['sudo', '-n', 'setfacl', '-m', `${uidEntry},m::rx`, DATA_PATH, accountDir]);
|
||||
if (!openUp.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
error:
|
||||
`could not grant ${params.email} read access to ${DATA_PATH}: ${openUp.out}. ` +
|
||||
`Without it their own tooling cannot resolve paths inside their home.`,
|
||||
};
|
||||
}
|
||||
|
||||
// Every sibling of `home` is the platform's. 700 means traversal alone does not open them.
|
||||
const entries = await readdir(accountDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
|
||||
Reference in New Issue
Block a user