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
