Skip to content

fix: preserve special file ownership from packages when cutting - #338

Open
chris-bainbridge wants to merge 1 commit into
canonical:mainfrom
chris-bainbridge:preserve-file-ownership
Open

chris-bainbridge wants to merge 1 commit into
canonical:mainfrom
chris-bainbridge:preserve-file-ownership

Conversation

@chris-bainbridge

Copy link
Copy Markdown
  • Have you signed the CLA?

Problem

Files extracted by chisel cut are always created with the ownership of the running process (root:root in build environments), discarding the uid/gid recorded in the package tarball. This silently breaks any file that relies on non-root ownership for its function.

Most notable victim: the PAM chkpwd helpers /usr/sbin/unix_chkpwd and /usr/sbin/pam_extrausers_chkpwd. These ship setgid shadow (mode 2755) so they can read the root:shadow, mode 0640 shadow files on behalf of an unprivileged PAM caller (the standard Debian/Ubuntu privilege-separation design). Cut with chisel they end up setgid root, which grants no such access, so every password check performed by a non-root caller fails — e.g. screen lock/unlock in a confined desktop session on Ubuntu Core, where the session snap's PAM stack execs the helpers from the base snap. The same flattening affects chage and expiry (shadow), crontab (crontab), plocate (plocate) and ssh-agent (_ssh).

The mode (including the setgid bit) is already preserved; only the uid/gid are dropped, in internal/tarball/extract.go, which builds fsutil.CreateOptions from the tar header without carrying the header's Uid/Gid over — internal/fsutil has no way to express them at all.

Fix

  • internal/fsutil: CreateOptions gains optional Uid/Gid, applied with os.Chown to newly created entries, and to pre-existing ones when OverrideMode is set (mirroring the existing mode semantics). Ownership is never applied to symlinks; hard links inherit it from their target's inode. Unprivileged runs that cannot chown log a single warning and keep the previous behavior.
  • internal/tarball: the tar header's uid/gid is carried through for entries and their parent directories — but only requested when it differs from root:root. Root-owned entries already end up root-owned when cutting as root, and cannot be chowned when cutting as a regular user, so the overwhelmingly common case keeps its previous behavior exactly (no extra syscalls, no behavior change for chiseled container images).

Slice definitions are unchanged; the ownership comes from the packages themselves, so cuts stay deterministic.

Verification

  • Unit tests: new cases in internal/fsutil (ownership applied/withheld per the mode semantics, symlink and hard link handling, CreateWriter, unprivileged EPERM warning) and internal/tarball (a setgid, gid-42 entry modeling unix_chkpwd; asserting preservation as root and the warning fallback as a regular user). Full suite passes both as root and as a regular user (17/17 packages).
  • End-to-end: cutting the real libpam-modules-bin_auth slice from chisel-ubuntu-core (26.04) as root now produces
    -rwxr-sr-x 0:42 usr/sbin/unix_chkpwd
    -rwxr-sr-x 0:42 usr/sbin/pam_extrausers_chkpwd
    
    matching a plain dpkg -x of the same package (previously root:root).

Files extracted from packages were always created with the ownership of
the running process (root:root in build environments), discarding the
uid/gid recorded in the package tarball. This breaks any file relying
on non-root ownership for its function: most notably the PAM
chkpwd helpers (/usr/sbin/unix_chkpwd and
/usr/sbin/pam_extrausers_chkpwd) ship setgid shadow so they can read
the root:shadow, mode 0640 shadow files on behalf of an unprivileged
PAM caller -- cut with chisel they end up setgid root, which grants no
such access, so every password check performed by a non-root caller
(e.g. screen lock/unlock in a confined desktop session) fails. The
same flattening silently affects chage, expiry (shadow), crontab
(crontab), plocate (plocate) and ssh-agent (_ssh).

Extend fsutil.CreateOptions with optional Uid/Gid, applied with
os.Chown to newly created entries (and pre-existing ones when
OverrideMode is set, mirroring the mode semantics). Ownership is never
applied to symlinks, and hard links inherit it from their target's
inode. Unprivileged runs that cannot chown log a single warning and
keep the previous behavior.

tarball.Extract now carries the tar header uid/gid through for entries
and their parent directories, but only requests ownership that differs
from root:root: root-owned entries already end up root-owned when
cutting as root, and cannot be chowned when cutting as a regular user,
so the common case keeps its previous behavior exactly.

Verified cutting the real libpam-modules-bin_auth slice from
chisel-ubuntu-core (26.04): the chkpwd helpers now come out
root:shadow 2755, matching a plain dpkg -x of the same package.

@upils upils 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.

Thanks @chris-bainbridge.
I think it addresses a real gap in Chisel. As stated in the comments below I think the approach could be refined to avoid tricky bugs but this is going in the right direction.
Even if it seems to fix Chisel to "do the right thing" I will think about the consequences of this difference in behavior.

Root: opts.TargetDir,
Path: links[0].path,
Mode: tarHeader.FileInfo().Mode(),
Data: tarReader,

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.

The proper UID/GID must also be given here. We might be in a situation where the path associated to the target file in the tarball was not extracted (because it was not requested by the selected slices) so we will extract its content to the first entry in the links list, as a regular file. And in that case we must respect the UID/GID.

Comment thread internal/fsutil/create.go
Comment on lines +33 to +43
// Uid and Gid, if not nil, are applied to the created entry with
// os.Chown, so the entry faithfully reflects the ownership recorded
// in the source package. A nil value leaves the corresponding id
// unchanged. Ownership is applied to newly created entries, and to
// pre-existing ones only when OverrideMode is true. It is never
// applied to symlinks (their ownership has no meaningful effect),
// nor to hard links (they share the inode, and therefore the
// ownership, of their already-created target). Applying ownership
// requires sufficient privilege (e.g. running as root); otherwise a
// warning is logged once and the entry keeps the ownership of the
// current process user and group.

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.

This is a summary of all the implementation. This is documenting pieces of behavior from multiple functions below. This may become misleading if/when the implementation changes but this comment is not updated. I know this is somehow consistent with other comments in this struct but it pushes it a bit far I think.

Comment thread internal/fsutil/create.go
Comment on lines +167 to +172
if existedErr != nil {
if err := applyOwnership(path, o); err != nil {
file.Close()
return nil, nil, err
}
}

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.

There is an issue doing that here. Because applyOwnership does a chown it strips the setgid. So what is recorded in the entry below may not reflect the actual mode of the file. In the other places this is fine "by chance" because a call to chmod is putting back the expected mode but not here. This is going unnoticed here as CreateWriter is only used to write the manifest but is it conceptually wrong.

Suggestions to fix:

  • In applyOwnership, after a successful chown, re-chmod regular files to o.Mode (restoring any setuid/setgid), or
  • Centralize: single applyOwnership call in Create placed before the existing OverrideMode chmod, and the same re-chmod in CreateWriter.

// before the entry for the file itself. This is the case for .deb files but
// not for all tarballs.
tarDirMode := make(map[string]fs.FileMode)
tarDirs := make(map[string]tarDirInfo)

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.

Suggested change
tarDirs := make(map[string]tarDirInfo)
tarDirInfos := make(map[string]tarDirInfo)

?

return int(stat.Uid), int(stat.Gid)
}

func (s *S) TestCreateOwnership(c *C) {

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.

A table-driven test would probably be easier to follow, avoid unwanted side effect of a previous case and better fit the project convention.

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