Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package apko for openSUSE:Factory checked in 
at 2026-09-28 10:47:49
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/apko (Old)
 and      /work/SRC/openSUSE:Factory/.apko.new.383539 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "apko"

Mon Sep 28 10:47:49 2026 rev:138 rq:1381018 version:1.4.5

Changes:
--------
--- /work/SRC/openSUSE:Factory/apko/apko.changes        2026-09-21 
12:07:56.399972402 +0200
+++ /work/SRC/openSUSE:Factory/.apko.new.383539/apko.changes    2026-09-28 
10:48:47.958851662 +0200
@@ -1,0 +2,11 @@
+Mon Sep 28 04:45:32 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 1.4.5:
+  * fix(apko): fix CON-2732 (#2516)
+  * build(deps): bump the codeql group with 2 updates (#2515)
+  * build(deps): bump go.step.sm/crypto from 0.90.0 to 0.91.0
+    (#2514)
+  * build(deps): bump chainguard.dev/sdk from 0.1.271 to 0.1.278
+    (#2513)
+
+-------------------------------------------------------------------

Old:
----
  apko-1.4.4.obscpio

New:
----
  apko-1.4.5.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ apko.spec ++++++
--- /var/tmp/diff_new_pack.0obrGA/_old  2026-09-28 10:48:49.992936887 +0200
+++ /var/tmp/diff_new_pack.0obrGA/_new  2026-09-28 10:48:49.994936971 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           apko
-Version:        1.4.4
+Version:        1.4.5
 Release:        0
 Summary:        Build OCI images from APK packages directly without Dockerfile
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.0obrGA/_old  2026-09-28 10:48:50.022938144 +0200
+++ /var/tmp/diff_new_pack.0obrGA/_new  2026-09-28 10:48:50.026938311 +0200
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/chainguard-dev/apko.git</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
-    <param name="revision">refs/tags/v1.4.4</param>
+    <param name="revision">refs/tags/v1.4.5</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.0obrGA/_old  2026-09-28 10:48:50.048939233 +0200
+++ /var/tmp/diff_new_pack.0obrGA/_new  2026-09-28 10:48:50.054939485 +0200
@@ -3,6 +3,6 @@
                 <param 
name="url">https://github.com/chainguard-dev/apko</param>
               <param 
name="changesrevision">861f83f69e6fa9114405a2f7bb5cf6585ad00421</param></service><service
 name="tar_scm">
                 <param 
name="url">https://github.com/chainguard-dev/apko.git</param>
-              <param 
name="changesrevision">7e72102a642f1ce74cbca37ef85f8b925d02a11b</param></service></servicedata>
+              <param 
name="changesrevision">739e7ce3f675ffb232e16849b51ff93657f1570c</param></service></servicedata>
 (No newline at EOF)
 

++++++ apko-1.4.4.obscpio -> apko-1.4.5.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.4/go.mod new/apko-1.4.5/go.mod
--- old/apko-1.4.4/go.mod       2026-09-20 23:42:11.000000000 +0200
+++ new/apko-1.4.5/go.mod       2026-09-22 00:49:38.000000000 +0200
@@ -3,7 +3,7 @@
 go 1.27.0
 
 require (
-       chainguard.dev/sdk v0.1.271
+       chainguard.dev/sdk v0.1.278
        github.com/chainguard-dev/clog v1.8.1
        github.com/charmbracelet/log v1.0.0
        github.com/erofs/go-erofs v0.3.2-0.20260804074615-52cc42c5291c
@@ -28,7 +28,7 @@
        go.lsp.dev/uri v1.0.1
        go.opentelemetry.io/otel v1.46.0
        go.opentelemetry.io/otel/trace v1.46.0
-       go.step.sm/crypto v0.90.0
+       go.step.sm/crypto v0.91.0
        golang.org/x/oauth2 v0.37.0
        golang.org/x/sync v0.23.0
        golang.org/x/sys v0.48.0
@@ -58,7 +58,7 @@
        github.com/cespare/xxhash/v2 v2.3.0 // indirect
        github.com/charmbracelet/colorprofile v0.4.3 // indirect
        github.com/charmbracelet/lipgloss v1.1.0 // indirect
-       github.com/charmbracelet/x/ansi v0.11.7 // indirect
+       github.com/charmbracelet/x/ansi v0.11.8 // indirect
        github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
        github.com/charmbracelet/x/term v0.2.2 // indirect
        github.com/clipperhouse/displaywidth v0.11.0 // indirect
@@ -95,7 +95,7 @@
        github.com/kelseyhightower/envconfig v1.4.0 // indirect
        github.com/kevinburke/ssh_config v1.6.0 // indirect
        github.com/klauspost/cpuid/v2 v2.4.0 // indirect
-       github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
+       github.com/lucasb-eyer/go-colorful v1.4.1 // indirect
        github.com/mattn/go-isatty v0.0.24 // indirect
        github.com/mattn/go-runewidth v0.0.24 // indirect
        github.com/moby/docker-image-spec v1.3.1 // indirect
@@ -114,7 +114,7 @@
        github.com/prometheus/procfs v0.21.1 // indirect
        github.com/rivo/uniseg v0.4.7 // indirect
        github.com/sergi/go-diff v1.4.0 // indirect
-       github.com/sirupsen/logrus v1.9.4 // indirect
+       github.com/sirupsen/logrus v1.10.1 // indirect
        github.com/skeema/knownhosts v1.3.2 // indirect
        github.com/spf13/pflag v1.0.10 // indirect
        github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
@@ -130,10 +130,10 @@
        go.opentelemetry.io/proto/otlp v1.11.0 // indirect
        go.yaml.in/yaml/v3 v3.0.5 // indirect
        go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect
-       golang.org/x/crypto v0.56.0 // indirect
+       golang.org/x/crypto v0.57.0 // indirect
        golang.org/x/exp v0.0.0-20260727155853-b88d891fe743 // indirect
-       golang.org/x/net v0.58.0 // indirect
-       golang.org/x/text v0.41.0 // indirect
+       golang.org/x/net v0.59.0 // indirect
+       golang.org/x/text v0.42.0 // indirect
        google.golang.org/genproto/googleapis/api 
v0.0.0-20260803160001-6ac0973c030d // indirect
        google.golang.org/genproto/googleapis/rpc 
v0.0.0-20260825221802-da73d73af1c5 // indirect
        google.golang.org/grpc v1.83.2 // indirect
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.4/go.sum new/apko-1.4.5/go.sum
--- old/apko-1.4.4/go.sum       2026-09-20 23:42:11.000000000 +0200
+++ new/apko-1.4.5/go.sum       2026-09-22 00:49:38.000000000 +0200
@@ -1,7 +1,7 @@
 chainguard.dev/go-grpc-kit v0.20.0 
h1:MDwZtTlUlMSMEPcoi9m8tnu4g+M88T1toUZ3Th/+0Gc=
 chainguard.dev/go-grpc-kit v0.20.0/go.mod 
h1:ocuwyRX9tqRRvJkS3/ZkDYGMnl+6FLC4bvF0cm6DGsk=
-chainguard.dev/sdk v0.1.271 h1:AzzWuDOuQ4mPPE4IFB5y2/Ii5vZMPFoiGEO1MCQnwFk=
-chainguard.dev/sdk v0.1.271/go.mod 
h1:qdToj7HJ0neJdut3yLmYC1TIfF6sp5RPmlFXgZ4r1kA=
+chainguard.dev/sdk v0.1.278 h1:vM5AkOAlrhMXpEQumITidz+w0nQxNIYAXbyPN5nqD6o=
+chainguard.dev/sdk v0.1.278/go.mod 
h1:qdToj7HJ0neJdut3yLmYC1TIfF6sp5RPmlFXgZ4r1kA=
 cloud.google.com/go/auth v0.23.2 
h1:pxSCpfiji41hpzpPdMCftEUCezpgpqmmDdYiAjCKXxo=
 cloud.google.com/go/auth v0.23.2/go.mod 
h1:4DhBRcqvtljQN3dJ57qtqbib5ZGCYE5f2crfiiC2EM0=
 cloud.google.com/go/auth/oauth2adapt v0.2.8 
h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
@@ -41,8 +41,8 @@
 github.com/charmbracelet/lipgloss v1.1.0/go.mod 
h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
 github.com/charmbracelet/log v1.0.0 
h1:HVVVMmfOorfj3BA9i8X8UL69Hoz9lI0PYwXfJvOdRc4=
 github.com/charmbracelet/log v1.0.0/go.mod 
h1:uYgY3SmLpwJWxmlrPwXvzVYujxis1vAKRV/0VQB7yWA=
-github.com/charmbracelet/x/ansi v0.11.7 
h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
-github.com/charmbracelet/x/ansi v0.11.7/go.mod 
h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
+github.com/charmbracelet/x/ansi v0.11.8 
h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
+github.com/charmbracelet/x/ansi v0.11.8/go.mod 
h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
 github.com/charmbracelet/x/cellbuf v0.0.15 
h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
 github.com/charmbracelet/x/cellbuf v0.0.15/go.mod 
h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
 github.com/charmbracelet/x/term v0.2.2 
h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
@@ -82,8 +82,8 @@
 github.com/emirpasic/gods v1.18.1/go.mod 
h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
 github.com/erofs/go-erofs v0.3.2-0.20260804074615-52cc42c5291c 
h1:l2NcPw9ioT/HNi0J3L4wiU2BcoYxiVx0xwWilF8S4p8=
 github.com/erofs/go-erofs v0.3.2-0.20260804074615-52cc42c5291c/go.mod 
h1:XkSeN9MHszGd4+3gcEjadJLYHCQpWzJ7/8yznzMuzJs=
-github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
-github.com/fatih/color v1.18.0/go.mod 
h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
+github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
+github.com/fatih/color v1.19.0/go.mod 
h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
 github.com/felixge/httpsnoop v1.1.0 
h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
 github.com/felixge/httpsnoop v1.1.0/go.mod 
h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
 github.com/gliderlabs/ssh v0.3.8 
h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
@@ -164,10 +164,10 @@
 github.com/kr/text v0.2.0/go.mod 
h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/kylelemons/godebug v1.1.0 
h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
 github.com/kylelemons/godebug v1.1.0/go.mod 
h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
-github.com/lucasb-eyer/go-colorful v1.4.0 
h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
-github.com/lucasb-eyer/go-colorful v1.4.0/go.mod 
h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
-github.com/mattn/go-colorable v0.1.14 
h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
-github.com/mattn/go-colorable v0.1.14/go.mod 
h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/lucasb-eyer/go-colorful v1.4.1 
h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss=
+github.com/lucasb-eyer/go-colorful v1.4.1/go.mod 
h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-colorable v0.1.15 
h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
+github.com/mattn/go-colorable v0.1.15/go.mod 
h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
 github.com/mattn/go-isatty v0.0.24 
h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
 github.com/mattn/go-isatty v0.0.24/go.mod 
h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
 github.com/mattn/go-runewidth v0.0.24 
h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
@@ -213,14 +213,14 @@
 github.com/prometheus/procfs v0.21.1/go.mod 
h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
 github.com/rivo/uniseg v0.4.7/go.mod 
h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
-github.com/rogpeppe/go-internal v1.14.1 
h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
-github.com/rogpeppe/go-internal v1.14.1/go.mod 
h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/rogpeppe/go-internal v1.16.0 
h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
+github.com/rogpeppe/go-internal v1.16.0/go.mod 
h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod 
h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
 github.com/sergi/go-diff v1.4.0/go.mod 
h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
 github.com/sirupsen/logrus v1.7.0/go.mod 
h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
-github.com/sirupsen/logrus v1.9.4 
h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
-github.com/sirupsen/logrus v1.9.4/go.mod 
h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
+github.com/sirupsen/logrus v1.10.1 
h1:xi4336Zh11WpU14fXR6I67V3yaTPQYwRx2WEtHbRg4Q=
+github.com/sirupsen/logrus v1.10.1/go.mod 
h1:vsQHnG7xzNsxk3NrwboUiWPnIC3dmbjcGPykD7+tiHk=
 github.com/skeema/knownhosts v1.3.2 
h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
 github.com/skeema/knownhosts v1.3.2/go.mod 
h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
 github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
@@ -276,8 +276,8 @@
 go.opentelemetry.io/otel/trace v1.46.0/go.mod 
h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
 go.opentelemetry.io/proto/otlp v1.11.0 
h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
 go.opentelemetry.io/proto/otlp v1.11.0/go.mod 
h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
-go.step.sm/crypto v0.90.0 h1:ZEWK0Ly0RyEC2S2OP1+N/SRbTRU+sW7go0tttHgyXkw=
-go.step.sm/crypto v0.90.0/go.mod 
h1:dgT4uZ4cClpjCi6HZZAmLomgn5tOfpHTqb34lTFN2PQ=
+go.step.sm/crypto v0.91.0 h1:0mN0DwVOvUuh7VbyTnxABZuAkxwak/Grp8Y/b4YaZDU=
+go.step.sm/crypto v0.91.0/go.mod 
h1:NxxObRBymdbyXLoTCW5Ea6sLxuy5yI/xr9+hSBlbU/Q=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod 
h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
@@ -291,22 +291,22 @@
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod 
h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod 
h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.19.0/go.mod 
h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
-golang.org/x/crypto v0.56.0/go.mod 
h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
+golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
+golang.org/x/crypto v0.57.0/go.mod 
h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
 golang.org/x/exp v0.0.0-20260727155853-b88d891fe743 
h1:ex206bKw+v3K0dm3andkrIF+ijyQKJG1pLgwQ2PYdQM=
 golang.org/x/exp v0.0.0-20260727155853-b88d891fe743/go.mod 
h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod 
h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74=
-golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY=
+golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
+golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod 
h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod 
h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod 
h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod 
h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
-golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
+golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
+golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
 golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
 golang.org/x/oauth2 v0.37.0/go.mod 
h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -343,16 +343,16 @@
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.14.0/go.mod 
h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
-golang.org/x/text v0.41.0/go.mod 
h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
+golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
+golang.org/x/text v0.42.0/go.mod 
h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
 golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
 golang.org/x/time v0.16.0/go.mod 
h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod 
h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod 
h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod 
h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod 
h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
-golang.org/x/tools v0.49.0/go.mod 
h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
+golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
+golang.org/x/tools v0.50.0/go.mod 
h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod 
h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
 gonum.org/v1/gonum v0.17.0/go.mod 
h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.4/pkg/apk/apk/empty_package_name_test.go 
new/apko-1.4.5/pkg/apk/apk/empty_package_name_test.go
--- old/apko-1.4.4/pkg/apk/apk/empty_package_name_test.go       2026-09-20 
23:42:11.000000000 +0200
+++ new/apko-1.4.5/pkg/apk/apk/empty_package_name_test.go       2026-09-22 
00:49:38.000000000 +0200
@@ -3,6 +3,7 @@
 import (
        "archive/tar"
        "errors"
+       "strings"
        "testing"
 )
 
@@ -82,3 +83,60 @@
                t.Errorf("added package name = %q, want %q", got, "minimal")
        }
 }
+
+// The read side range-checks the owner on an "M:"/"a:" line, and that error
+// aborts the read of the whole database rather than just the bad record. So 
the
+// write side has to refuse the same values, for the same reason the empty name
+// and the top-level-directory cases above are refused: a record we cannot read
+// back takes everything else down with it.
+func TestAddInstalledPackageRejectsOutOfRangeOwner(t *testing.T) {
+       cases := []struct {
+               name     string
+               uid, gid int64
+               errMatch string
+       }{
+               {"uid 2^32", 1 << 32, 0, "invalid uid 4294967296"},
+               {"negative uid", -1, 0, "invalid uid -1"},
+               {"gid 2^32", 0, 1 << 32, "invalid gid 4294967296"},
+               {"negative gid", 0, -1, "invalid gid -1"},
+       }
+       for _, tt := range cases {
+               t.Run(tt.name, func(t *testing.T) {
+                       // Skip the test when the ids don't fit an int. On a 
32-bit platform
+                       // archive/tar rejects it before this code sees it.
+                       uid, gid := idsAsIntOrSkip(t, tt.uid, tt.gid)
+
+                       a, _, err := testGetTestAPK()
+                       if err != nil {
+                               t.Fatalf("testGetTestAPK: %v", err)
+                       }
+                       before, err := a.GetInstalled()
+                       if err != nil {
+                               t.Fatalf("GetInstalled: %v", err)
+                       }
+
+                       files := []tar.Header{
+                               {Name: "usr", Typeflag: tar.TypeDir, Mode: 
0o755},
+                               {Name: "usr/bin", Typeflag: tar.TypeDir, Mode: 
0o755},
+                               {Name: "usr/bin/backdoor", Typeflag: 
tar.TypeReg, Mode: 0o4755, Uid: uid, Gid: gid, Size: 5},
+                       }
+
+                       _, err = a.AddInstalledPackage(&Package{Name: 
"backdoor", Version: "1.0", Arch: "x86_64"}, files)
+                       if err == nil {
+                               t.Fatal("AddInstalledPackage accepted an 
out-of-range owner, want rejection")
+                       }
+                       if !strings.Contains(err.Error(), tt.errMatch) {
+                               t.Errorf("error = %q, want it to contain %q", 
err.Error(), tt.errMatch)
+                       }
+
+                       // The database must still be readable, which is the 
whole point.
+                       after, err := a.GetInstalled()
+                       if err != nil {
+                               t.Fatalf("GetInstalled after rejection: %v", 
err)
+                       }
+                       if len(after) != len(before) {
+                               t.Errorf("installed package count went %d -> %d 
after a rejected write", len(before), len(after))
+                       }
+               })
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.4/pkg/apk/apk/gen_owner_width_fixture.go 
new/apko-1.4.5/pkg/apk/apk/gen_owner_width_fixture.go
--- old/apko-1.4.4/pkg/apk/apk/gen_owner_width_fixture.go       1970-01-01 
01:00:00.000000000 +0100
+++ new/apko-1.4.5/pkg/apk/apk/gen_owner_width_fixture.go       2026-09-22 
00:49:38.000000000 +0200
@@ -0,0 +1,114 @@
+//go:build ignore
+
+// Generates testdata/owner-width.tar, the fixture behind
+// TestOwnerWidthBoundaries.
+//
+// It has to be a generator rather than table rows built in the test, because
+// archive/tar's writer cannot express these values on a 32-bit platform:
+// Header.Uid is an int, and tar.Writer derives the uid/gid PAX records from
+// that field, ignoring any the caller supplies in PAXRecords. Running it once
+// on a 64-bit host and committing the bytes means the test reads what a real
+// tar producer emits, on every architecture.
+//
+// This is a limitation of the Go writer API, not of tar or of 32-bit hosts. 
PAX
+// records are ASCII text and GNU base-256 is a byte encoding; both represent
+// uids far beyond uint32, and a 32-bit process writing the header blocks by
+// hand produces these archives fine (verified). An attacker is under no
+// obligation to use archive/tar, so nothing here narrows the threat -- only 
the
+// consumer's word size matters.
+//
+//     go run gen_owner_width_fixture.go
+package main
+
+import (
+       "archive/tar"
+       "fmt"
+       "log"
+       "os"
+       "strconv"
+)
+
+// The declared uid/gid values, chosen to cover every boundary where a width or
+// sign change bites: the uint32 range ends, the int32 range ends, and the
+// narrowing that archive/tar performs on a 32-bit int wraps past both.
+var declared = []int64{
+       0,          // root, legitimate
+       1,          // ordinary, legitimate
+       65534,      // nobody, legitimate
+       2147483647, // MaxInt32   -- legitimate, largest value a 32-bit int 
holds
+       2147483648, // MaxInt32+1 -- legitimate uint32, overflows a 32-bit int
+       4294967295, // MaxUint32  -- legitimate, largest value apko may accept
+       4294967296, // 2^32       -- MUST be rejected; narrows to 0 on a 32-bit 
int
+       4294968296, // 2^32+1000  -- MUST be rejected; narrows to 1000
+       -1,         // MUST be rejected; the classic wrap to MaxUint32
+}
+
+func main() {
+       f, err := os.Create("testdata/owner-width.tar")
+       if err != nil {
+               log.Fatal(err)
+       }
+       defer f.Close()
+
+       tw := tar.NewWriter(f)
+       content := []byte("#!/bin/sh\n")
+
+       for _, format := range []struct {
+               name string
+               f    tar.Format
+       }{
+               {"pax", tar.FormatPAX},
+               {"gnu", tar.FormatGNU},
+       } {
+               for _, id := range declared {
+                       // The declared value travels in the entry name so the 
test needs no
+                       // side-channel: whatever archive/tar reports for Uid 
on the reading
+                       // platform, the name still says what the producer 
wrote. Mode is
+                       // 04755 throughout -- an owner that narrows to 0 on a 
setuid binary
+                       // is the whole point.
+                       name := fmt.Sprintf("usr/bin/%s-uid-%s", format.name, 
strconv.FormatInt(id, 10))
+                       hdr := &tar.Header{
+                               Name:     name,
+                               Typeflag: tar.TypeReg,
+                               Mode:     0o4755,
+                               Uid:      int(id),
+                               Gid:      0,
+                               Size:     int64(len(content)),
+                               Format:   format.f,
+                       }
+                       if int64(hdr.Uid) != id {
+                               log.Fatalf("%s: uid %d is not representable in 
an int on this host; "+
+                                       "generate the fixture on a 64-bit 
machine", name, id)
+                       }
+                       if err := tw.WriteHeader(hdr); err != nil {
+                               log.Fatalf("WriteHeader(%s): %v", name, err)
+                       }
+                       if _, err := tw.Write(content); err != nil {
+                               log.Fatalf("Write(%s): %v", name, err)
+                       }
+
+                       // Same value in the gid field, so neither bound goes 
unexercised.
+                       gname := fmt.Sprintf("usr/bin/%s-gid-%s", format.name, 
strconv.FormatInt(id, 10))
+                       ghdr := &tar.Header{
+                               Name:     gname,
+                               Typeflag: tar.TypeReg,
+                               Mode:     0o4755,
+                               Uid:      0,
+                               Gid:      int(id),
+                               Size:     int64(len(content)),
+                               Format:   format.f,
+                       }
+                       if err := tw.WriteHeader(ghdr); err != nil {
+                               log.Fatalf("WriteHeader(%s): %v", gname, err)
+                       }
+                       if _, err := tw.Write(content); err != nil {
+                               log.Fatalf("Write(%s): %v", gname, err)
+                       }
+               }
+       }
+
+       if err := tw.Close(); err != nil {
+               log.Fatal(err)
+       }
+       fmt.Println("wrote testdata/owner-width.tar")
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.4/pkg/apk/apk/install.go 
new/apko-1.4.5/pkg/apk/apk/install.go
--- old/apko-1.4.4/pkg/apk/apk/install.go       2026-09-20 23:42:11.000000000 
+0200
+++ new/apko-1.4.5/pkg/apk/apk/install.go       2026-09-22 00:49:38.000000000 
+0200
@@ -25,6 +25,7 @@
        "fmt"
        "io"
        "io/fs"
+       "math"
        "os"
        "slices"
        "strings"
@@ -175,6 +176,57 @@
        return true, nil
 }
 
+// checkOwner rejects a header whose uid or gid does not fit a uint32.
+// archive/tar decodes PAX and GNU base-256 numbers into an int, so a crafted
+// APK can carry -1 or 2^32 in these fields. Everything downstream that stores
+// an owner, EROFS inodes included, holds a uint32, where those become
+// 4294967295 and 0 -- a root-owned file the package never declared, and with
+// setuid set, a root shell.
+//
+// This is the range check on its own, for a header whose fields the caller
+// supplied. A header that archive/tar decoded has to go through
+// checkArchiveOwner instead.
+//
+// The int64 casts are only what lets the comparison compile where int is 32
+// bits; math.MaxUint32 is an untyped constant that overflows such an int.
+func checkOwner(h *tar.Header) error {
+       if h.Uid < 0 || int64(h.Uid) > math.MaxUint32 {
+               return fmt.Errorf("invalid uid %d for %s: must be between 0 and 
%d", h.Uid, h.Name, uint32(math.MaxUint32))
+       }
+       if h.Gid < 0 || int64(h.Gid) > math.MaxUint32 {
+               return fmt.Errorf("invalid gid %d for %s: must be between 0 and 
%d", h.Gid, h.Name, uint32(math.MaxUint32))
+       }
+       return nil
+}
+
+// checkArchiveOwner is checkOwner for a header that came out of archive/tar,
+// where the range check alone cannot be trusted.
+//
+// None of it can be enforced unless an int holds every uint32. On a 32-bit
+// platform archive/tar does not reject an over-large owner, it truncates one:
+// mergePAX does hdr.Uid = int(id64) and readHeader does
+// int(p.parseNumeric(...)), both marked "Integer overflow possible" in the
+// stdlib. A declared uid of 2^32 therefore arrives as 0 and would satisfy 
every
+// bound in checkOwner, and what it was narrowed from is unrecoverable -- a GNU
+// base-256 owner carries no PAX record to re-read, and an expanded APK keeps
+// the narrowed value. Refuse outright rather than vouch for an owner that was
+// never checked. goreleaser ships linux/386, so this is a reachable build.
+//
+// The guard belongs here and not in checkOwner because AddInstalledPackage
+// range-checks headers a caller handed it, which need not have come from a tar
+// at all. Refusing those on a 32-bit build would reject ownership the decoder
+// never touched -- and it would catch nothing extra, because on such a build
+// the install paths below already refuse, so no decoded header ever reaches 
the
+// installed-database writer.
+func checkArchiveOwner(h *tar.Header) error {
+       if math.MaxInt < math.MaxUint32 {
+               return fmt.Errorf("cannot validate the owner of %s: archive/tar 
truncates uid/gid to an int, "+
+                       "so on this platform a declared value above %d is 
silently narrowed and an out-of-range "+
+                       "owner cannot be detected; validating package ownership 
requires a 64-bit build", h.Name, math.MaxInt)
+       }
+       return checkOwner(h)
+}
+
 // installAPKFiles install the files from the APK and return the list of 
installed files
 // and their permissions. Returns a tar.Header because it is a convenient 
existing
 // struct that has all of the fields we need.
@@ -231,6 +283,10 @@
                // whatever it is now, it is in the data section
                startedDataSection = true
 
+               if err := checkArchiveOwner(header); err != nil {
+                       return nil, err
+               }
+
                switch header.Typeflag {
                case tar.TypeDir:
                        // special case, if the target already exists, and it 
is a symlink to a directory, we can accept it as is
@@ -436,6 +492,15 @@
                // whatever it is now, it is in the data section
                startedDataSection = true
 
+               // Same check as the streaming path. memFS.WriteHeader happens 
not to
+               // copy the header's owner onto the node today, but the headers 
returned
+               // here go straight to AddInstalledPackage, which writes them 
into the
+               // installed database, and anything that wires ownership 
through -- the
+               // natural completion of the mode/ownership work -- lands on a 
uint32.
+               if err := checkArchiveOwner(&hdr); err != nil {
+                       return nil, err
+               }
+
                installed, err := wh.WriteHeader(hdr, src, pkg)
                if err != nil {
                        return nil, err
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.4/pkg/apk/apk/install_test.go 
new/apko-1.4.5/pkg/apk/apk/install_test.go
--- old/apko-1.4.4/pkg/apk/apk/install_test.go  2026-09-20 23:42:11.000000000 
+0200
+++ new/apko-1.4.5/pkg/apk/apk/install_test.go  2026-09-22 00:49:38.000000000 
+0200
@@ -26,11 +26,13 @@
        "fmt"
        "io"
        "io/fs"
+       "math"
        "os"
        "path/filepath"
        "slices"
        "sync"
        "testing"
+       "testing/fstest"
        "text/template"
 
        "github.com/stretchr/testify/assert"
@@ -48,6 +50,7 @@
 }
 
 func TestInstallAPKFiles(t *testing.T) {
+       skipWithoutOwnerValidation(t)
        t.Run("basic", func(t *testing.T) {
                apk, src, err := testGetTestAPK()
                require.NoErrorf(t, err, "failed to get test APK")
@@ -490,6 +493,7 @@
 // install path has to carry over from the tar headers: the mode bits outside
 // of Perm(), and the ownership.
 func TestInstallAPKFilesModesAndOwnership(t *testing.T) {
+       skipWithoutOwnerValidation(t)
        type entry struct {
                name    string
                mode    int64 // POSIX mode bits, as they appear in a tar header
@@ -570,6 +574,127 @@
        }
 }
 
+// skipWithoutOwnerValidation skips a test that installs package contents on a
+// platform where checkArchiveOwner refuses every decoded header. Installing is
+// deliberately impossible there -- archive/tar has already narrowed the owner
+// and apko will not vouch for it -- so these tests have nothing to assert
+// rather than something that broke. TestCheckArchiveOwnerRequires64BitInt
+// covers the refusal itself.
+func skipWithoutOwnerValidation(t *testing.T) {
+       t.Helper()
+       if math.MaxInt < math.MaxUint32 {
+               t.Skip("installing requires owner validation, which requires a 
64-bit int")
+       }
+}
+
+// idsAsIntOrSkip narrows a row's uid/gid to an int for a tar.Header field,
+// skipping the row where the value has no int form on this platform. Rows that
+// do fit still run: a negative owner is representable everywhere, and it is
+// checkOwner's other bound.
+func idsAsIntOrSkip(t *testing.T, uid, gid int64) (int, int) {
+       t.Helper()
+       u, g := int(uid), int(gid)
+       if int64(u) != uid || int64(g) != gid {
+               t.Skipf("uid %d/gid %d are not representable in an int on this 
platform", uid, gid)
+       }
+       return u, g
+}
+
+// TestCheckArchiveOwnerRequires64BitInt pins the guard itself. archive/tar
+// narrows uid/gid to an int on the way in ("Integer overflow possible" in
+// mergePAX and readHeader), so where an int is 32 bits a declared uid of 2^32
+// arrives as 0 and is indistinguishable from a package that really declared
+// root. An unvalidatable owner has to be an error, not a pass.
+//
+// The guard is on checkArchiveOwner rather than checkOwner because only a
+// decoded header is suspect; see the comment there.
+func TestCheckArchiveOwnerRequires64BitInt(t *testing.T) {
+       h := &tar.Header{Name: "usr/bin/ok", Uid: 1000, Gid: 1000}
+
+       err := checkArchiveOwner(h)
+       if math.MaxInt < math.MaxUint32 {
+               require.ErrorContains(t, err, "requires a 64-bit build")
+       } else {
+               require.NoError(t, err)
+       }
+
+       // checkOwner itself must stay usable on every platform: 
AddInstalledPackage
+       // calls it with headers a caller built, which archive/tar never 
touched.
+       require.NoError(t, checkOwner(h), "checkOwner must not inherit the 
archive guard")
+}
+
+// TestInstallAPKFilesRejectsOutOfRangeOwner: archive/tar reads PAX uid/gid
+// records into an int, so a crafted APK can declare an owner outside the
+// uint32 range. The EROFS writer truncates to uint32, turning 2^32 into 0, so 
a
+// 04755 file with uid 2^32 would come out setuid root. Such a header must fail
+// the install rather than reach any Chown.
+func TestInstallAPKFilesRejectsOutOfRangeOwner(t *testing.T) {
+       skipWithoutOwnerValidation(t)
+       // uid/gid are int64 here, not int: 1<<32 is an untyped constant that 
does
+       // not fit an int where int is 32 bits, and goreleaser builds 
linux/386. The
+       // rows that overflow are skipped there rather than made to compile, 
because
+       // the scenario is unrepresentable in a tar.Header.Uid on that platform 
--
+       // archive/tar rejects the value before any of this code sees it.
+       cases := []struct {
+               name     string
+               uid, gid int64
+               errMatch string
+       }{
+               {"uid 2^32", 1 << 32, 0, "invalid uid 4294967296"},
+               {"negative uid", -1, 0, "invalid uid -1"},
+               {"gid 2^32", 0, 1 << 32, "invalid gid 4294967296"},
+               {"negative gid", 0, -1, "invalid gid -1"},
+       }
+       for _, tt := range cases {
+               t.Run(tt.name, func(t *testing.T) {
+                       // Skip the test when the ids don't fit an int. On a 
32-bit platform
+                       // archive/tar rejects it before this code sees it.
+                       uid, gid := idsAsIntOrSkip(t, tt.uid, tt.gid)
+
+                       apk, src, err := testGetTestAPK()
+                       require.NoError(t, err)
+
+                       var buf bytes.Buffer
+                       tw := tar.NewWriter(&buf)
+                       require.NoError(t, tw.WriteHeader(&tar.Header{Name: 
"usr", Typeflag: tar.TypeDir, Mode: 0o755}))
+                       require.NoError(t, tw.WriteHeader(&tar.Header{Name: 
"usr/bin", Typeflag: tar.TypeDir, Mode: 0o755}))
+                       content := []byte("#!/bin/sh\n")
+                       require.NoError(t, tw.WriteHeader(&tar.Header{
+                               Name:     "usr/bin/backdoor",
+                               Typeflag: tar.TypeReg,
+                               Mode:     0o4755,
+                               Uid:      uid,
+                               Gid:      gid,
+                               Size:     int64(len(content)),
+                       }))
+                       _, err = tw.Write(content)
+                       require.NoError(t, err)
+                       require.NoError(t, tw.Close())
+
+                       // Make sure the header really carries the out-of-range 
value and
+                       // archive/tar did not clamp or reject it on the way 
in; otherwise
+                       // this test would pass for the wrong reason.
+                       tr := tar.NewReader(bytes.NewReader(buf.Bytes()))
+                       for {
+                               hdr, err := tr.Next()
+                               require.NoError(t, err)
+                               if hdr.Name == "usr/bin/backdoor" {
+                                       require.Equal(t, uid, hdr.Uid)
+                                       require.Equal(t, gid, hdr.Gid)
+                                       break
+                               }
+                       }
+
+                       _, err = apk.installAPKFiles(context.Background(), 
bytes.NewReader(buf.Bytes()), &Package{Origin: ""})
+                       require.Error(t, err)
+                       assert.Contains(t, err.Error(), tt.errMatch)
+
+                       _, err = src.Stat("usr/bin/backdoor")
+                       assert.ErrorIs(t, err, fs.ErrNotExist, "file with 
out-of-range owner must not be installed")
+               })
+       }
+}
+
 // TestInstallAPKFilesModesOnDisk is the same concern as
 // TestInstallAPKFilesModesAndOwnership, against a disk-backed filesystem.
 // Those strip setuid/setgid/sticky from the mode passed to MkdirAll and
@@ -577,6 +702,7 @@
 // is not asserted: Chown needs privileges the test does not have, and the
 // filesystem tolerates the EPERM.
 func TestInstallAPKFilesModesOnDisk(t *testing.T) {
+       skipWithoutOwnerValidation(t)
        dir := t.TempDir()
        src := apkfs.DirFS(t.Context(), dir)
        require.NotNil(t, src)
@@ -659,6 +785,7 @@
 // tolerates the EPERM from the disk chown and never reaches the kernel
 // behavior, so the call order is pinned here instead.
 func TestInstallAPKFilesMetadataOrder(t *testing.T) {
+       skipWithoutOwnerValidation(t)
        rec := newOrderRecordingFS(apkfs.NewMemFS())
        apk, err := New(t.Context(), WithFS(rec), 
WithIgnoreMknodErrors(ignoreMknodErrors))
        require.NoError(t, err)
@@ -682,3 +809,47 @@
                        "metadata calls for %s must be chown then chmod", name)
        }
 }
+
+// TestLazilyInstallAPKFilesRejectsOutOfRangeOwner is the companion to
+// TestInstallAPKFilesRejectsOutOfRangeOwner on the path apko build actually
+// takes: installPackage dispatches to lazilyInstallAPKFiles whenever the
+// filesystem is a WriteHeaderer, which pkg/tarfs is, and which is what
+// apko build, apko publish and apko build-minirootfs all construct. The
+// headers this returns go on to AddInstalledPackage, so an out-of-range owner
+// has to be refused before the entry is written rather than recorded and
+// rejected later by the read side.
+func TestLazilyInstallAPKFilesRejectsOutOfRangeOwner(t *testing.T) {
+       skipWithoutOwnerValidation(t)
+       cases := []struct {
+               name     string
+               uid, gid int64
+               errMatch string
+       }{
+               {"uid 2^32", 1 << 32, 0, "invalid uid 4294967296"},
+               {"negative uid", -1, 0, "invalid uid -1"},
+               {"gid 2^32", 0, 1 << 32, "invalid gid 4294967296"},
+               {"negative gid", 0, -1, "invalid gid -1"},
+       }
+       for _, tt := range cases {
+               t.Run(tt.name, func(t *testing.T) {
+                       // Skip the test when the ids don't fit an int. On a 
32-bit platform
+                       // archive/tar rejects it before this code sees it.
+                       uid, gid := idsAsIntOrSkip(t, tt.uid, tt.gid)
+
+                       apk, _, err := testGetTestAPK()
+                       require.NoError(t, err)
+
+                       entries := []tar.Header{
+                               {Name: "usr", Typeflag: tar.TypeDir, Mode: 
0o755},
+                               {Name: "usr/bin", Typeflag: tar.TypeDir, Mode: 
0o755},
+                               {Name: "usr/bin/backdoor", Typeflag: 
tar.TypeReg, Mode: 0o4755, Uid: uid, Gid: gid, Size: 10},
+                       }
+
+                       wh := &recordingWriteHeaderer{}
+                       _, err = apk.lazilyInstallAPKFiles(t.Context(), wh, 
entries, fstest.MapFS{}, &Package{Name: "backdoor"})
+                       require.Error(t, err)
+                       assert.Contains(t, err.Error(), tt.errMatch)
+                       assert.NotContains(t, wh.written, "usr/bin/backdoor", 
"entry with out-of-range owner must not be written")
+               })
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.4/pkg/apk/apk/installed.go 
new/apko-1.4.5/pkg/apk/apk/installed.go
--- old/apko-1.4.4/pkg/apk/apk/installed.go     2026-09-20 23:42:11.000000000 
+0200
+++ new/apko-1.4.5/pkg/apk/apk/installed.go     2026-09-22 00:49:38.000000000 
+0200
@@ -22,6 +22,7 @@
        "errors"
        "fmt"
        "io"
+       "math"
        "os"
        "path/filepath"
        "sort"
@@ -88,6 +89,17 @@
        // file lines
        topDirNeeded := true
        for _, f := range sortedFiles {
+               // Same invariant as the empty-name refusal above: don't write 
a record
+               // we cannot read back. parseInstalledPerms range-checks the 
owner on an
+               // "M:"/"a:" line, so an out-of-range uid here would not merely 
be wrong,
+               // it would abort the read of the whole database -- and
+               // hasUsrMergeBaseImage swallows that error and picks the wrong 
layout.
+               if err := checkOwner(&f); err != nil {
+                       return nil, fmt.Errorf("refusing to record ownership 
for package %q: %w: "+
+                               "the installed database cannot express it and 
the resulting record "+
+                               "would make the whole database unreadable", 
pkg.Name, err)
+               }
+
                perm := f.Mode & 0o7777
                user := f.Uid
                group := f.Gid
@@ -505,10 +517,18 @@
        if err != nil {
                return 0, 0, 0, fmt.Errorf("invalid permission string uid was 
not an integer %s", permString)
        }
+       // int64 cast: see checkOwner. uid is an int because it lands in
+       // tar.Header.Uid, and math.MaxUint32 overflows an int where int is 32 
bits.
+       if uid < 0 || int64(uid) > math.MaxUint32 {
+               return 0, 0, 0, fmt.Errorf("invalid permission string uid out 
of range %s", permString)
+       }
        gid, err = strconv.Atoi(permParts[1])
        if err != nil {
                return 0, 0, 0, fmt.Errorf("invalid permission string gid was 
not an integer %s", permString)
        }
+       if gid < 0 || int64(gid) > math.MaxUint32 {
+               return 0, 0, 0, fmt.Errorf("invalid permission string gid out 
of range %s", permString)
+       }
        perms, err = strconv.ParseInt(permParts[2], 8, 64)
        if err != nil {
                return 0, 0, 0, fmt.Errorf("invalid permission string perms was 
not an int64 %s", permString)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.4/pkg/apk/apk/installed_test.go 
new/apko-1.4.5/pkg/apk/apk/installed_test.go
--- old/apko-1.4.4/pkg/apk/apk/installed_test.go        2026-09-20 
23:42:11.000000000 +0200
+++ new/apko-1.4.5/pkg/apk/apk/installed_test.go        2026-09-22 
00:49:38.000000000 +0200
@@ -27,6 +27,7 @@
        "io"
        "os"
        "sort"
+       "strconv"
        "strings"
        "testing"
        "time"
@@ -1236,34 +1237,49 @@
 }
 
 func TestParseInstalledPerms(t *testing.T) {
+       // uid/gid are int64 rather than int because 4294967295 does not fit an 
int
+       // where int is 32 bits, and goreleaser builds linux/386. needs64Bit 
marks
+       // the rows whose permStr carries a value above MaxInt32: 
parseInstalledPerms
+       // reads it with Atoi into an int, so on those platforms Atoi rejects it
+       // first and the row describes a scenario that cannot arise. The 
negative
+       // rows are portable and run everywhere.
        cases := []struct {
-               name     string
-               permStr  string
-               uid      int
-               gid      int
-               perms    int64
-               errMatch string
+               name       string
+               permStr    string
+               uid        int64
+               gid        int64
+               perms      int64
+               errMatch   string
+               needs64Bit bool
        }{
-               {"executable file", "0:0:755", 0, 0, 0755, ""},
-               {"setuid executable file", "0:0:4755", 0, 0, 04755, ""},
-               {"non-root owner", "1001:0:644", 1001, 0, 0644, ""},
-               {"non-root group", "0:1001:644", 0, 1001, 0644, ""},
-               {"other-write perm", "0:0:777", 0, 0, 0777, ""},
-               {"too many tokens", "0:0:0:0", 0, 0, 0, "3 parts"},
-               {"bad uid token", "a:0:777", 0, 0, 0, "invalid.*uid"},
-               {"bad gid token", "0:b:7770", 0, 0, 0, "invalid.*gid"},
-               {"bad perm token", "0:0:cat", 0, 0, 0, "invalid.*perms"},
+               {name: "executable file", permStr: "0:0:755", perms: 0755},
+               {name: "setuid executable file", permStr: "0:0:4755", perms: 
04755},
+               {name: "non-root owner", permStr: "1001:0:644", uid: 1001, 
perms: 0644},
+               {name: "non-root group", permStr: "0:1001:644", gid: 1001, 
perms: 0644},
+               {name: "other-write perm", permStr: "0:0:777", perms: 0777},
+               {name: "too many tokens", permStr: "0:0:0:0", errMatch: "3 
parts"},
+               {name: "bad uid token", permStr: "a:0:777", errMatch: 
"invalid.*uid"},
+               {name: "bad gid token", permStr: "0:b:7770", errMatch: 
"invalid.*gid"},
+               {name: "max uid and gid", permStr: "4294967295:4294967295:644", 
uid: 4294967295, gid: 4294967295, perms: 0644, needs64Bit: true},
+               {name: "uid 2^32", permStr: "4294967296:0:644", errMatch: "uid 
out of range", needs64Bit: true},
+               {name: "negative uid", permStr: "-1:0:644", errMatch: "uid out 
of range"},
+               {name: "gid 2^32", permStr: "0:4294967296:644", errMatch: "gid 
out of range", needs64Bit: true},
+               {name: "negative gid", permStr: "0:-1:644", errMatch: "gid out 
of range"},
+               {name: "bad perm token", permStr: "0:0:cat", errMatch: 
"invalid.*perms"},
        }
        for _, tt := range cases {
                t.Run(tt.name, func(t *testing.T) {
+                       if tt.needs64Bit && strconv.IntSize < 64 {
+                               t.Skip("permission string carries a uid/gid 
above MaxInt32, unrepresentable in an int here")
+                       }
                        uid, gid, perms, err := parseInstalledPerms(tt.permStr)
                        if tt.errMatch != "" {
                                require.Error(t, err, "expected error found 
none")
                                assert.Regexp(t, tt.errMatch, err.Error(), 
"Error message should match the regex")
                                return
                        }
-                       assert.Equal(t, tt.uid, uid, "unexpected uid")
-                       assert.Equal(t, tt.gid, gid, "unexpected gid")
+                       assert.Equal(t, tt.uid, int64(uid), "unexpected uid")
+                       assert.Equal(t, tt.gid, int64(gid), "unexpected gid")
                        assert.Equal(t, tt.perms, perms, "unexpected perms")
                })
        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.4/pkg/apk/apk/owner_width_test.go 
new/apko-1.4.5/pkg/apk/apk/owner_width_test.go
--- old/apko-1.4.4/pkg/apk/apk/owner_width_test.go      1970-01-01 
01:00:00.000000000 +0100
+++ new/apko-1.4.5/pkg/apk/apk/owner_width_test.go      2026-09-22 
00:49:38.000000000 +0200
@@ -0,0 +1,153 @@
+package apk
+
+import (
+       "archive/tar"
+       "errors"
+       "io"
+       "math"
+       "os"
+       "strconv"
+       "strings"
+       "testing"
+)
+
+// TestOwnerWidthBoundaries walks the whole uid/gid boundary matrix from a
+// fixture produced by a real archive/tar writer (see
+// gen_owner_width_fixture.go), and asserts that checkArchiveOwner's verdict 
matches
+// what the producer *declared* -- not what the reading platform happens to
+// report after narrowing.
+//
+// That distinction is the entire point. tar.Header.Uid is an int, and
+// archive/tar narrows into it before apko ever sees the header:
+// reader.go mergePAX does `hdr.Uid = int(id64) // Integer overflow possible`
+// for PAX records, and readHeader does `int(p.parseNumeric(...))` for GNU
+// base-256. On a 32-bit platform a declared uid of 2^32 therefore arrives as 
0,
+// satisfies `0 <= uid <= MaxUint32`, and reaches Chown as root -- on a 04755
+// file, a setuid-root binary the package never declared. linux/386 is a 
shipped
+// goreleaser target, so this is not hypothetical.
+//
+// Both encodings are covered because a PAX record leaves the true value behind
+// in hdr.PAXRecords["uid"], which a fix could consult, while GNU base-256
+// carries no such record and cannot be recovered after narrowing.
+//
+// Run it on both widths; passing on amd64 alone proves nothing here:
+//
+//     go test ./pkg/apk/apk/ -run TestOwnerWidthBoundaries
+//     GOARCH=386 go test ./pkg/apk/apk/ -run TestOwnerWidthBoundaries
+func TestOwnerWidthBoundaries(t *testing.T) {
+       f, err := os.Open("testdata/owner-width.tar")
+       if err != nil {
+               t.Fatalf("open fixture: %v", err)
+       }
+       defer f.Close()
+
+       tr := tar.NewReader(f)
+       seen := 0
+       for {
+               hdr, err := tr.Next()
+               if errors.Is(err, io.EOF) {
+                       break
+               }
+               if err != nil {
+                       t.Fatalf("reading fixture: %v", err)
+               }
+
+               format, field, declared, ok := parseOwnerWidthName(hdr.Name)
+               if !ok {
+                       t.Fatalf("fixture entry %q does not encode a declared 
id; regenerate it", hdr.Name)
+               }
+               seen++
+
+               t.Run(format+"/"+field+"/"+strconv.FormatInt(declared, 10), 
func(t *testing.T) {
+                       err := checkArchiveOwner(hdr)
+                       gotAccept := err == nil
+
+                       // Where an int cannot hold every uint32, archive/tar 
has already
+                       // destroyed the declared value and no per-value 
verdict is
+                       // possible. checkArchiveOwner refuses everything 
instead, so that is what
+                       // this asserts -- including for owners that would be 
perfectly
+                       // legitimate on a 64-bit build.
+                       if math.MaxInt < math.MaxUint32 {
+                               if gotAccept {
+                                       t.Errorf("declared %s=%d was ACCEPTED 
on a %d-bit int platform, want refused.\n"+
+                                               "  entry:    %s (mode %04o)\n"+
+                                               "  observed: hdr value %d -- 
indistinguishable from a package that declared it",
+                                               field, declared, 
strconv.IntSize, hdr.Name, hdr.Mode&0o7777, ownerField(hdr, field))
+                               } else if !strings.Contains(err.Error(), 
"requires a 64-bit build") {
+                                       t.Errorf("declared %s=%d was refused, 
but not by the width guard.\n"+
+                                               "  error: %v\n"+
+                                               "  want it to explain that 
validation needs a 64-bit build, so the "+
+                                               "operator can tell an 
unvalidatable platform from a bad package",
+                                               field, declared, err)
+                               }
+                               return
+                       }
+
+                       // A uid is representable iff it fits a uint32. 
Anything else the
+                       // package declared must be refused.
+                       wantAccept := declared >= 0 && declared <= 
math.MaxUint32
+
+                       if gotAccept == wantAccept {
+                               return
+                       }
+
+                       observed := hdr.Uid
+                       if field == "gid" {
+                               observed = hdr.Gid
+                       }
+                       narrowed := int64(observed) != declared
+
+                       switch {
+                       case gotAccept && !wantAccept:
+                               t.Errorf("declared %s=%d was ACCEPTED, want 
rejected.\n"+
+                                       "  entry:      %s (mode %04o)\n"+
+                                       "  observed:   hdr.%s=%d (narrowed=%t, 
int is %d bits)\n"+
+                                       "  PAX record: %q\n"+
+                                       "  downstream: EROFS Chown stores 
uint32(%d) = %d\n"+
+                                       "  => a package declaring an 
unrepresentable owner installs as uid %d",
+                                       field, declared, hdr.Name, 
hdr.Mode&0o7777,
+                                       strings.ToUpper(field[:1])+field[1:], 
observed, narrowed, strconv.IntSize,
+                                       hdr.PAXRecords[field], observed, 
uint32(observed), uint32(observed)) //nolint:gosec // demonstrating the 
truncation
+                       case !gotAccept && wantAccept:
+                               t.Errorf("declared %s=%d was REJECTED, want 
accepted.\n"+
+                                       "  entry:      %s\n"+
+                                       "  observed:   hdr.%s=%d (narrowed=%t, 
int is %d bits)\n"+
+                                       "  error:      %v\n"+
+                                       "  => a legitimate uint32 owner is 
unusable on this platform",
+                                       field, declared, hdr.Name,
+                                       strings.ToUpper(field[:1])+field[1:], 
observed, narrowed, strconv.IntSize,
+                                       err)
+                       }
+               })
+       }
+
+       // 2 formats x 9 values x 2 fields. A silently short fixture would turn 
this
+       // whole test into a no-op, which is exactly the failure mode it exists 
to
+       // prevent elsewhere.
+       if want := 36; seen != want {
+               t.Errorf("fixture yielded %d entries, want %d; regenerate with 
`go run gen_owner_width_fixture.go`", seen, want)
+       }
+}
+
+// parseOwnerWidthName pulls the format, field and declared id back out of a
+// fixture entry name of the form "usr/bin/<format>-<field>-<id>".
+func parseOwnerWidthName(name string) (format, field string, declared int64, 
ok bool) {
+       base := name[strings.LastIndex(name, "/")+1:]
+       parts := strings.SplitN(base, "-", 3)
+       if len(parts) != 3 {
+               return "", "", 0, false
+       }
+       id, err := strconv.ParseInt(parts[2], 10, 64)
+       if err != nil {
+               return "", "", 0, false
+       }
+       return parts[0], parts[1], id, true
+}
+
+// ownerField returns the uid or gid the reading platform actually produced.
+func ownerField(h *tar.Header, field string) int {
+       if field == "gid" {
+               return h.Gid
+       }
+       return h.Uid
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.4/pkg/apk/apk/path_traversal_test.go 
new/apko-1.4.5/pkg/apk/apk/path_traversal_test.go
--- old/apko-1.4.4/pkg/apk/apk/path_traversal_test.go   2026-09-20 
23:42:11.000000000 +0200
+++ new/apko-1.4.5/pkg/apk/apk/path_traversal_test.go   2026-09-22 
00:49:38.000000000 +0200
@@ -248,6 +248,7 @@
 // overlay and is what downstream layer/tar/cpio emitters consume through
 // dirFS.Stat().Mode() and DirEntry.Info().Mode().
 func TestInstallSetuidBinary(t *testing.T) {
+       skipWithoutOwnerValidation(t)
        ctx := t.Context()
 
        sandbox := t.TempDir()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.4/pkg/apk/apk/root_directory_entry_test.go 
new/apko-1.4.5/pkg/apk/apk/root_directory_entry_test.go
--- old/apko-1.4.4/pkg/apk/apk/root_directory_entry_test.go     2026-09-20 
23:42:11.000000000 +0200
+++ new/apko-1.4.5/pkg/apk/apk/root_directory_entry_test.go     2026-09-22 
00:49:38.000000000 +0200
@@ -177,6 +177,7 @@
 // AddInstalledPackage, and it is that composition -- not a synthetic header
 // list -- that hung before the parent-walk fix.
 func TestInstallPathRejectsUnrepresentableRootPermissions(t *testing.T) {
+       skipWithoutOwnerValidation(t)
        ctx := t.Context()
        base := filepath.Join(t.TempDir(), "base")
        fsys := apkfs.DirFS(ctx, base, apkfs.WithCreateDir())
Binary files old/apko-1.4.4/pkg/apk/apk/testdata/owner-width.tar and 
new/apko-1.4.5/pkg/apk/apk/testdata/owner-width.tar differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.4/pkg/build/accounts_test.go 
new/apko-1.4.5/pkg/build/accounts_test.go
--- old/apko-1.4.4/pkg/build/accounts_test.go   2026-09-20 23:42:11.000000000 
+0200
+++ new/apko-1.4.5/pkg/build/accounts_test.go   2026-09-22 00:49:38.000000000 
+0200
@@ -17,6 +17,7 @@
 import (
        "testing"
 
+       apkfs "chainguard.dev/apko/pkg/apk/fs"
        "chainguard.dev/apko/pkg/build/types"
 )
 
@@ -78,3 +79,45 @@
                }
        }
 }
+
+// mutateAccounts reads /etc/passwd, appends the configured users and writes 
the
+// whole file back. Before parseID, a package-shipped line with an out-of-range
+// uid parsed cleanly, truncated to 0 and was written back out as a root entry 
—
+// the read-modify-write is what turned a bad parse into a persisted root
+// account. The parse error has to propagate out of mutateAccounts instead.
+func Test_mutateAccounts_rejectsOutOfRangeUID(t *testing.T) {
+       for _, test := range []struct {
+               desc   string
+               passwd string
+       }{
+               {"uid 2^32", 
"backdoor:x:4294967296:0:backdoor:/dev/null:/sbin/nologin\n"},
+               {"negative uid", 
"backdoor:x:-1:0:backdoor:/dev/null:/sbin/nologin\n"},
+               {"gid 2^32", 
"backdoor:x:1000:4294967296:backdoor:/dev/null:/sbin/nologin\n"},
+       } {
+               t.Run(test.desc, func(t *testing.T) {
+                       fsys := apkfs.NewMemFS()
+                       if err := fsys.MkdirAll("etc", 0o755); err != nil {
+                               t.Fatalf("MkdirAll: %v", err)
+                       }
+                       if err := fsys.WriteFile("etc/passwd", 
[]byte(test.passwd), 0o644); err != nil {
+                               t.Fatalf("WriteFile: %v", err)
+                       }
+
+                       ic := &types.ImageConfiguration{}
+                       ic.Accounts.Users = []types.User{{UserName: "nonroot", 
UID: 65532, GID: id0T}}
+
+                       if err := mutateAccounts(fsys, ic); err == nil {
+                               t.Fatal("mutateAccounts accepted an 
out-of-range id, want rejection")
+                       }
+
+                       // The bad line must not have been rewritten as a root 
entry.
+                       got, err := fsys.ReadFile("etc/passwd")
+                       if err != nil {
+                               t.Fatalf("ReadFile: %v", err)
+                       }
+                       if string(got) != test.passwd {
+                               t.Errorf("etc/passwd was rewritten:\n got 
%q\nwant %q", got, test.passwd)
+                       }
+               })
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.4/pkg/passwd/group.go 
new/apko-1.4.5/pkg/passwd/group.go
--- old/apko-1.4.4/pkg/passwd/group.go  2026-09-20 23:42:11.000000000 +0200
+++ new/apko-1.4.5/pkg/passwd/group.go  2026-09-22 00:49:38.000000000 +0200
@@ -20,7 +20,6 @@
        "io"
        "io/fs"
        "os"
-       "strconv"
        "strings"
 
        apkfs "chainguard.dev/apko/pkg/apk/fs"
@@ -129,11 +128,11 @@
        ge.GroupName = parts[0]
        ge.Password = parts[1]
 
-       gid, err := strconv.Atoi(parts[2])
+       gid, err := parseID("GID", parts[2])
        if err != nil {
-               return fmt.Errorf("failed to parse UID %s", parts[2])
+               return err
        }
-       ge.GID = uint32(gid)
+       ge.GID = gid
 
        ge.Members = strings.Split(parts[3], ",")
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.4/pkg/passwd/group_test.go 
new/apko-1.4.5/pkg/passwd/group_test.go
--- old/apko-1.4.4/pkg/passwd/group_test.go     2026-09-20 23:42:11.000000000 
+0200
+++ new/apko-1.4.5/pkg/passwd/group_test.go     2026-09-22 00:49:38.000000000 
+0200
@@ -65,6 +65,35 @@
        assert.True(t, found_nobody, "group file should contain the nobody 
group")
 }
 
+// TestGroupParseGIDRange is the /etc/group side of TestParseIDRange: a gid of
+// 2^32 must not become the root group.
+func TestGroupParseGIDRange(t *testing.T) {
+       cases := []struct {
+               name     string
+               line     string
+               gid      uint32
+               errMatch string
+       }{
+               {"control", "nginx:x:101:nginx", 101, ""},
+               {"max gid", "big:x:4294967295:", 4294967295, ""},
+               {"gid 2^32", "backdoor:x:4294967296:", 0, `GID "4294967296"`},
+               {"negative gid", "backdoor:x:-1:", 0, `GID "-1"`},
+       }
+       for _, tt := range cases {
+               t.Run(tt.name, func(t *testing.T) {
+                       ge := GroupEntry{}
+                       err := ge.Parse(tt.line)
+                       if tt.errMatch != "" {
+                               require.Error(t, err)
+                               assert.Contains(t, err.Error(), tt.errMatch)
+                               return
+                       }
+                       require.NoError(t, err)
+                       assert.Equal(t, tt.gid, ge.GID)
+               })
+       }
+}
+
 func TestGroupWriter(t *testing.T) {
        fsys := apkfs.DirFS(t.Context(), "testdata")
        gf, err := ReadOrCreateGroupFile(fsys, "group")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.4/pkg/passwd/passwd.go 
new/apko-1.4.5/pkg/passwd/passwd.go
--- old/apko-1.4.4/pkg/passwd/passwd.go 2026-09-20 23:42:11.000000000 +0200
+++ new/apko-1.4.5/pkg/passwd/passwd.go 2026-09-22 00:49:38.000000000 +0200
@@ -19,6 +19,7 @@
        "fmt"
        "io"
        "io/fs"
+       "math"
        "os"
        "strconv"
        "strings"
@@ -133,17 +134,17 @@
        ue.UserName = parts[0]
        ue.Password = parts[1]
 
-       uid, err := strconv.Atoi(parts[2])
+       uid, err := parseID("UID", parts[2])
        if err != nil {
-               return fmt.Errorf("failed to parse UID %s", parts[2])
+               return err
        }
-       ue.UID = uint32(uid)
+       ue.UID = uid
 
-       gid, err := strconv.Atoi(parts[3])
+       gid, err := parseID("GID", parts[3])
        if err != nil {
-               return fmt.Errorf("failed to parse GID %s", parts[3])
+               return err
        }
-       ue.GID = uint32(gid)
+       ue.GID = gid
 
        ue.Info = parts[4]
        ue.HomeDir = parts[5]
@@ -152,6 +153,19 @@
        return nil
 }
 
+// parseID parses a uid or gid field. Only values that fit a uint32 are
+// accepted: a bare int conversion would turn -1 into 4294967295 and 2^32 into
+// 0, so an entry that never claimed to be root would be written back out as
+// root. The entries come from the packages being installed, so that has to be
+// an error rather than a wrap.
+func parseID(field, s string) (uint32, error) {
+       id, err := strconv.ParseUint(s, 10, 32)
+       if err != nil {
+               return 0, fmt.Errorf("failed to parse %s %q: must be an integer 
between 0 and %d", field, s, uint32(math.MaxUint32))
+       }
+       return uint32(id), nil
+}
+
 // Write writes an /etc/passwd line into an io.Writer.
 func (ue *UserEntry) Write(w io.Writer) error {
        _, err := fmt.Fprintf(w, "%s:%s:%d:%d:%s:%s:%s\n", ue.UserName, 
ue.Password, ue.UID, ue.GID, ue.Info, ue.HomeDir, ue.Shell)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.4.4/pkg/passwd/passwd_test.go 
new/apko-1.4.5/pkg/passwd/passwd_test.go
--- old/apko-1.4.4/pkg/passwd/passwd_test.go    2026-09-20 23:42:11.000000000 
+0200
+++ new/apko-1.4.5/pkg/passwd/passwd_test.go    2026-09-22 00:49:38.000000000 
+0200
@@ -58,6 +58,43 @@
        assert.True(t, found_nobody, "passwd file should contain the nobody 
user")
 }
 
+// TestParseIDRange pins the uid/gid range check. Before it, 2^32 parsed and
+// truncated to 0 and -1 wrapped to 4294967295, so a passwd line from a
+// package could turn into a root entry in the generated /etc/passwd.
+func TestParseIDRange(t *testing.T) {
+       cases := []struct {
+               name     string
+               line     string
+               uid, gid uint32
+               errMatch string
+       }{
+               {"control", 
"nginx:x:100:101:nginx:/var/lib/nginx:/sbin/nologin", 100, 101, ""},
+               {"max uid and gid", 
"big:x:4294967295:4294967295::/:/sbin/nologin", 4294967295, 4294967295, ""},
+               {"uid 2^32", 
"backdoor:x:4294967296:100:svc:/var/lib/svc:/sbin/nologin", 0, 0, `UID 
"4294967296"`},
+               {"negative uid", 
"backdoor:x:-1:100:svc:/var/lib/svc:/sbin/nologin", 0, 0, `UID "-1"`},
+               {"gid 2^32", 
"backdoor:x:100:4294967296:svc:/var/lib/svc:/sbin/nologin", 0, 0, `GID 
"4294967296"`},
+               {"negative gid", 
"backdoor:x:100:-1:svc:/var/lib/svc:/sbin/nologin", 0, 0, `GID "-1"`},
+       }
+       for _, tt := range cases {
+               t.Run(tt.name, func(t *testing.T) {
+                       ue := UserEntry{}
+                       err := ue.Parse(tt.line)
+                       if tt.errMatch != "" {
+                               require.Error(t, err)
+                               assert.Contains(t, err.Error(), tt.errMatch)
+                               return
+                       }
+                       require.NoError(t, err)
+                       assert.Equal(t, tt.uid, ue.UID)
+                       assert.Equal(t, tt.gid, ue.GID)
+
+                       w := &bytes.Buffer{}
+                       require.NoError(t, ue.Write(w))
+                       assert.Equal(t, tt.line+"\n", w.String(), "entry should 
round-trip unchanged")
+               })
+       }
+}
+
 func TestWriter(t *testing.T) {
        fsys := apkfs.NewMemFS()
        passwd, err := os.ReadFile("testdata/passwd")

++++++ apko.obsinfo ++++++
--- /var/tmp/diff_new_pack.0obrGA/_old  2026-09-28 10:48:50.455956286 +0200
+++ /var/tmp/diff_new_pack.0obrGA/_new  2026-09-28 10:48:50.458956412 +0200
@@ -1,5 +1,5 @@
 name: apko
-version: 1.4.4
-mtime: 1789940531
-commit: 7e72102a642f1ce74cbca37ef85f8b925d02a11b
+version: 1.4.5
+mtime: 1790030978
+commit: 739e7ce3f675ffb232e16849b51ff93657f1570c
 

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/apko/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.apko.new.383539/vendor.tar.gz differ: char 137, 
line 2

Reply via email to