Skip to content

fix(garm): set USER/LOGNAME in the runner .env to the UID account - #437

Merged
yanksyoon merged 6 commits into
mainfrom
fix/garm-user-env-alias
Oct 21, 2026
Merged

yanksyoon merged 6 commits into
mainfrom
fix/garm-user-env-alias

Conversation

@yanksyoon

@yanksyoon yanksyoon commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

What this PR does

Writes USER and LOGNAME to the runner .env for every GARM scale set, set to the account the runner's UID resolves to. The account is resolved on the instance, not hardcoded.

Also adds the missing assertion to the GARM e2e job: it runs on a real GARM-spawned runner and now checks that the job identity matches its UID and that usermod -aG followed by sg succeeds.

Why we need it

GARM starts the runner as runner, which the runner images create as a duplicate-UID alias of ubuntu:

/etc/passwd                     first entry wins for getpwuid
  ubuntu:x:1000:1000  <- first
  runner:x:1000:1000  <- alias, same uid and gid
/etc/group
  ubuntu:x:1000:      runner:x:1000:
/home/runner -> /home/ubuntu

One process, two names, and group membership is stored per name:

  $USER, $LOGNAME   ->  "runner"
  getpwuid(1000)    ->  "ubuntu"
  sg's name lookup  ->  "ubuntu"

So a step that hands $USER to a UID-keyed tool writes one name and is checked as the other:

  sudo usermod -aG sbuild $USER        # $USER = "runner"
      -> /etc/group: sbuild:x:1234:runner

  sg sbuild -c 'mk-sbuild noble'
      -> gid 1234 not in this shell's groups (shell predates the usermod)
      -> caller name = getpwuid(1000) = "ubuntu"
      -> "ubuntu" in [runner]?  no
      -> Password:            (ubuntu is locked)
      -> Invalid password.    exit 1

This is the Setup build prerequisites failure in canonical/toolchains-ci on every GARM pool. Legacy runners pass because su - ubuntu makes both sides ubuntu.

Fix. The runner applies <runner root>/.env to its own environment before any job starts (actions/runner, src/Runner.Listener/Program.cs, LoadAndSetEnv), and steps inherit it. The charm already delivers ACTIONS_RUNNER_HOOK_JOB_STARTED, DOCKERHUB_MIRROR and the tmate variables that way.

  .env:  USER=<account the runner UID resolves to>
         LOGNAME=<same>

  $USER                    ->  same account
  getpwuid(1000)           ->  same account
  usermod -aG sbuild $USER ->  sbuild:x:1234:<same account>
  sg sbuild                ->  match, no password

Where it lands. Injected into github_linux_charmed, so it reaches every GARM scale set:

  github_linux (GARM system template)
      |
      v
  github_linux_charmed  + USER/LOGNAME  + tmate vars
      |                    |
      |                    +--> scale sets with runner options copy it
      |                         as the base for their own template
      |
      +--> scale sets without runner options reference it directly

What this does not fix

The alias remains; only $USER and $LOGNAME change. Removal of this override is tracked in #438.

  • Literal runner. usermod -aG sbuild runner followed by sg sbuild still ends in Password: Invalid password.
  • Name-keyed state. Membership lands on the account the UID resolves to, while GARM's account stays runner: getent group docker lists ubuntu, sudo -l -U runner does not, and name-keyed policy (limits.conf, pam_access, getpwnam("runner")) is evaluated against ubuntu.
  • Anything started outside a job step. tmate/debug-ssh sessions, systemd-run, systemctl --user, cron, docker exec and container steps do not inherit .env.
  • logname / getlogin(). Still empty — no login session, as today.
  • Reach. Only pools built from github_linux_charmed. If the charm cannot reach GARM, the unit reports WaitingStatus before ScalesetReconciler runs: existing scale sets keep whatever template they already reference and no new ones are created, so the block is not silently dropped from live scale sets.

Test plan

  • tox -e unit: 449 passed
  • tox -e lint, tox -e static, tox -e fmt: clean
  • Rendered the snippet with and without a debug-ssh connection and checked it with bash -n
  • Dry-ran the new e2e steps on a noble host against both account layouts: the identity assertion exits 1 with USER=runner (today's layout, before the fix) and 0 with USER=ubuntu; the usermod -aG + sg step passes; the layout report prints 2 passwd entry/entries for the alias layout and 1 once runner has its own UID
  • E2E (promotion pipeline, not a merge gate): garm_e2e_test_run.yaml now asserts, on a real GARM-spawned runner, that $USER and LOGNAME equal the UID-resolved account and that usermod -aG <group> "$USER" then sg <group> succeeds — the pattern from the original report. Before this PR that job printed whoami but asserted nothing, so the disagreeing value sat in the logs unnoticed.

New unit coverage: no account name is baked into the template, the lookup is emitted, both assignments go through the format string, an empty lookup is guarded, the tmate block keeps its quoted heredoc, the block lands before the base set -e, and exactly one USER assignment is written.

Review focus

  • The account pair uses printf while the tmate block keeps a quoted heredoc: the account value must expand on the instance, relation-provided values must not.
  • id -u runner is used instead of id -u, because cloud-init runs the template as root.
  • On an image where runner has its own UID the resolved value is runner, so the override becomes a no-op rather than targeting the wrong account.

Potential breaking changes

USER/LOGNAME change from runner to ubuntu in job steps on the current images. Both names share uid 1000 and gid 1000, so this is name-only for chown, sudo -u and usermod; commands that record the name (group files, logs) now record the one id -un already returned.

Checklist

  • Changes comply with the project's coding standards and guidelines (see CONTRIBUTING.md and STYLE.md)
  • CONTRIBUTING.md has been updated upon changes to the contribution/development process — not applicable
  • Technical author has been assigned to review the PR in case of documentation changes — not applicable, changelog only
  • I updated docs/changelog.md with user-relevant changes
  • I used AI to assist with preparing this PR
  • I added or updated tests as needed (unit and integration)
  • Integration test modules: not applicable
  • Grafana dashboard: not applicable
  • Terraform: not applicable
  • Rockcraft: not applicable
  • AGENTS.md: not applicable, no charm or repo-structure change
  • .copilot-collections.yaml / .github/instructions/: not applicable

GARM starts the runner as `runner`, which the runner images create as a
duplicate-UID alias of `ubuntu` (useradd --non-unique --uid "$(id -u ubuntu)",
home /home/ubuntu, /home/runner -> /home/ubuntu). Job steps therefore read
$USER=runner, while every lookup keyed on the UID - getpwuid(1000), and with it
sg, newgrp, chown, and sbuild's schroot user - resolves to `ubuntu`, the first
entry for that UID in /etc/passwd.

A step that passes $USER to one of those tools acted on a different account than
the one the tool checked. Concretely, canonical/toolchains-ci does:

    sudo usermod -aG sbuild $USER   # adds runner
    sg sbuild -c 'mk-sbuild ...'    # checks ubuntu -> password prompt

and fails with "Password: Invalid password." because the ubuntu account has a
locked password. This affects any workflow using $USER with a UID-based tool,
not just sbuild.

The runner applies <runner root>/.env to its own environment before any job
starts (actions/runner src/Runner.Listener/Program.cs, LoadAndSetEnv), so
writing USER and LOGNAME there makes $USER agree with the passwd entry the UID
resolves to, and steps inherit it.

The snippet is injected into github_linux_charmed, which scale sets without
runner options reference directly; scale sets with runner options copy that
template as the base for their own, so both get the variables.

Values are written once per boot and are stripped before being rewritten, so a
reused instance cannot accumulate duplicate keys.
@yanksyoon

Copy link
Copy Markdown
Member Author

We need to further discuss what the long term solution to this underlying paradigm of two users would be (ubuntu user and runner user).

@cbartz cbartz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense as a stopgap: on the current images it only aligns $USER/$LOGNAME with what whoami/id -un already report, and it matches the legacy runners' ubuntu environment. Two things I'd like before merge, plus a few nits.

To address before merge

  • Resolve the account at runtime instead of hardcoding ubuntu. The override is applied to every scale set, but it is only correct on images where runner is a duplicate-UID alias of ubuntu. On an image where runner has its own UID, USER=ubuntu would be wrong and usermod -aG <group> $USER would modify the wrong account. Resolving on the instance, e.g. getent passwd "$(id -u runner)" | cut -d: -f1 with an unquoted heredoc, is a no-op on non-aliased images, correct on the current ones, and drops the cross-repo RUNNER_UID_USER constant.

  • Open a tracking issue for removing this override and link it from the code. This is a workaround for the duplicate-UID layout of the runner images, and the long-term fix is still to be decided at the design meeting. An issue gives that discussion a home and gives this override a planned removal. Without one, a charm-wide change to $USER tends to stay forever. Referencing it from the build_runner_env_snippet docstring (and the changelog entry) tells the next reader when it can go.

Nits

  • PR description, "What this does not fix": a CharmedTemplateError doesn't make scale sets fall back to the base template. _reconcile raises before ScalesetReconciler runs, the unit goes to WaitingStatus, and existing scale sets keep whatever template they already reference.
  • The PR body doesn't follow .github/pull_request_template.md ("What this PR does" / "Why we need it" / checklist). The existing content maps onto it easily, and the checklist (changelog, tests, AI assistance) is worth ticking.
  • See the inline comments.

AI-generated message.

Comment thread charms/garm/src/garm_template.py Outdated
Comment thread charms/garm/src/garm_template.py Outdated
Comment thread charms/garm/src/garm_template.py Outdated
Comment thread charms/garm/src/garm_template.py Outdated
Comment thread charms/garm/tests/unit/test_garm_template.py Outdated
Comment thread docs/changelog.md Outdated
GARM starts the runner as `runner`, which the runner images create as a
duplicate-UID alias of `ubuntu`. Job steps therefore read $USER=runner while
every lookup keyed on the UID - getpwuid(1000), and with it sg, newgrp, chown
and sbuild's schroot user - resolves to `ubuntu`. A step that hands $USER to one
of those tools acts on a different account than the tool checks, e.g.
toolchains-ci's `usermod -aG sbuild $USER` followed by `sg sbuild`, which fails
with "Password: Invalid password.".

The runner applies <runner root>/.env to its own environment before any job
starts (actions/runner Program.cs, LoadAndSetEnv), so writing USER and LOGNAME
there makes $USER agree with the passwd entry the UID resolves to. The account is
resolved on the instance rather than hardcoded, so the snippet stays correct if
the image gives the account its own UID.

One writer now renders the whole .env block: the account pair is written with
printf because the value must expand on the instance, and the tmate variables
keep their quoted heredoc so relation-provided values are not expanded.

Removal is tracked in #438.
@yanksyoon
yanksyoon force-pushed the fix/garm-user-env-alias branch from 3528fee to de3dd9c Compare October 21, 2026 05:48
@yanksyoon
yanksyoon marked this pull request as ready for review October 21, 2026 06:20
The e2e job runs on a GARM-spawned runner, the only place the duplicate-UID
account layout shows up at job level. It printed whoami but never compared it
with $USER, so the toolchains-ci failure ("Password: Invalid password.") went
unnoticed even though the disagreeing value was in the log.

Adds three steps:
- assert $USER and LOGNAME equal the account the UID resolves to
- assert usermod -aG <group> "$USER" then sg <group> succeeds, the pattern from
  the report; the step is a new process whose groups predate the usermod, so sg
  must resolve the caller from the UID
- report the runner uid/gid entry counts so #438 can tell when the .env override
  is no longer needed (non-gating, independent of the invoking account)
A parallel session committed task.md (an unrelated security-group plan) and a
'#'-prefixed note inside build_env_snippet's docstring. The note duplicates the
TODO(#438) comment below it and reads as literal docstring text.

task.md stays on disk, untracked; only its tracking is removed.
@yanksyoon
yanksyoon requested a review from cbartz October 21, 2026 09:48

@cbartz cbartz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving: all points from my earlier review are addressed. The account is now looked up on the instance, so the override does nothing on images where runner has its own UID; removal is tracked in #438; and the .env writers are merged into one. I checked GARM's github_linux template: it runs chown -R on the runner directory and chown on .env itself after install, so creating .env as root first doesn't affect ownership.

Recommendation before merging: please dispatch the GARM e2e run (garm_e2e_test_run.yaml) on this branch. So far the fix has been tested with unit tests and a dry run on a noble host. The new identity and usermod -aG + sg checks are the only way to confirm that the runner applies the .env override to job steps on a real GARM-spawned runner, including in agent mode.

AI-generated message.

@yanksyoon
yanksyoon merged commit 02c4a26 into main Oct 21, 2026
63 checks passed
@yanksyoon
yanksyoon deleted the fix/garm-user-env-alias branch October 21, 2026 10:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants