trailofbits / trailofbits/coop

Profile install scripts run before the guest user is created

Open
#381 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
243
Forks
13
Avg merge
1d 20h
Merged PRs (30d)
30

Description

Summary

Profile pre_install / post_install scripts run before the guest user is created, so a profile that writes into the guest user's home directory (e.g. installing dotfiles) either fails or writes into the wrong place. The files land in the base CI rootfs's leftover /home/ubuntu, which is then orphaned when guest-config.sh replaces the uid-1000 user.

Root cause

compose_recipe in src/setup.rs assembles the chroot provisioning script in this order:

  1. preamble + export GUEST_USER=...
  2. base packages
  3. profile pre-install scripts (src/setup.rs:664)
  4. third-party repos + packages
  5. profile post-install scripts (src/setup.rs:704)
  6. guest-config.shthis is where the guest user is created (src/setup.rs:720, script at scripts/guest/guest-config.sh:61-95)
  7. claude-code / codex

So at steps 3 and 5 the guest user and its /home/<user> do not exist yet. The comment at src/setup.rs:719 shows the placement was only chosen to be "before claude-code" — profile scripts that depend on the user were never accounted for.

Symptoms

Two users / home directories are visible in the guest:

  • <guest_user> / /home/<guest_user> — the configured guest user, created by guest-config.sh at step 6.
  • ubuntu / /home/ubuntu — ships with the base CI rootfs image (not created by coop at launch).

When a custom guest user is configured, guest-config.sh evicts the pre-existing uid-1000 user:

# scripts/guest/guest-config.sh:69-75
EXISTING=$(getent passwd 1000 | cut -d: -f1) || true
if [[ -n "$EXISTING" ]]; then
    userdel "$EXISTING"          # no -r → /home/ubuntu is left on disk
fi
useradd -m -s /bin/bash --uid 1000 -G sudo,docker "${GUEST_USER}"

Two leaks:

  1. Only the uid-1000 occupant is removed. If the base image's ubuntu is not at uid 1000, it survives as a full account alongside <guest_user>.
  2. userdel is called without -r, so /home/ubuntu is left behind as an orphan even when the account is removed.

A profile installing dotfiles at step 5 finds no /home/<guest_user>. If it falls back to (or hardcodes) /home/ubuntu, the files land there — and are then stranded when step 6 deletes/replaces the uid-1000 user, leaving the real guest home empty.

Fix

Create the guest user before profile scripts run. guest-config.sh cannot simply be moved to the top, because it also symlinks the claude binary that is not installed until step 7. Split it instead:

  1. Extract the user + home creation block (scripts/guest/guest-config.sh:61-95: useradd/usermod, sudoers, /home/<user> ownership, .local tree, .ssh keys) into its own script segment.
  2. Run that segment in compose_recipe right after base packages and before pre_installs (src/setup.rs:663).
  3. Leave the rest of guest-config.sh (networking, docker daemon, claude symlink, services, sshd) where it is at src/setup.rs:720.
  4. Add -r to the userdel so the orphan /home/ubuntu does not linger.
  5. Add a unit test asserting the useradd line precedes profile pre/post-install in the composed recipe (guards against regressing the ordering).

Acceptance criteria

  • A profile post_install (or pre_install) script can write to /home/<guest_user> and have it persist.
  • No orphaned /home/ubuntu remains after provisioning with a custom guest user.
  • A test pins the ordering: guest-user creation before profile scripts.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/setup.rs at compose_recipe and inspect the guest-config.sh user-creation block at scripts/guest/guest-config.sh:61-95. Trace how the recipe places pre_install, post_install, and guest-config steps, then review the existing unit-test patterns before adding an ordering assertion. Done means profile scripts can persist files in /home/<guest_user>, no orphaned /home/ubuntu remains, and the ordering test passes.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust, shell
Domain
cli, infrastructure
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.