Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package melange for openSUSE:Factory checked in at 2026-03-11 20:55:49 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/melange (Old) and /work/SRC/openSUSE:Factory/.melange.new.8177 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "melange" Wed Mar 11 20:55:49 2026 rev:145 rq:1338204 version:0.45.3 Changes: -------- --- /work/SRC/openSUSE:Factory/melange/melange.changes 2026-03-10 17:58:31.512460502 +0100 +++ /work/SRC/openSUSE:Factory/.melange.new.8177/melange.changes 2026-03-11 20:57:22.627015435 +0100 @@ -1,0 +2,13 @@ +Wed Mar 11 06:00:38 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 0.45.3: + * qemu: add SSH keepalives, handshake timeouts, and error + wrapping (#2412) + * qemu: set conservative guest MTU to prevent hangs in nested + environments (#2411) +- Update to version 0.45.2: + * chore: Defer initramfs deletion until after SSH client is set + up (#2410) + * go: upgrade to mody/mody/v2 (#2408) + +------------------------------------------------------------------- Old: ---- melange-0.45.1.obscpio New: ---- melange-0.45.3.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ melange.spec ++++++ --- /var/tmp/diff_new_pack.Sh5Z5m/_old 2026-03-11 20:57:24.251082439 +0100 +++ /var/tmp/diff_new_pack.Sh5Z5m/_new 2026-03-11 20:57:24.251082439 +0100 @@ -17,7 +17,7 @@ Name: melange -Version: 0.45.1 +Version: 0.45.3 Release: 0 Summary: Build APKs from source code License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.Sh5Z5m/_old 2026-03-11 20:57:24.287083924 +0100 +++ /var/tmp/diff_new_pack.Sh5Z5m/_new 2026-03-11 20:57:24.291084089 +0100 @@ -3,7 +3,7 @@ <param name="url">https://github.com/chainguard-dev/melange</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v0.45.1</param> + <param name="revision">v0.45.3</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.Sh5Z5m/_old 2026-03-11 20:57:24.343086235 +0100 +++ /var/tmp/diff_new_pack.Sh5Z5m/_new 2026-03-11 20:57:24.347086400 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/chainguard-dev/melange</param> - <param name="changesrevision">049664c63b543602ba8e344d2ebf0bdd950f3428</param></service></servicedata> + <param name="changesrevision">2a226faac364a05b188a3afcbbdc45b35b395ff3</param></service></servicedata> (No newline at EOF) ++++++ melange-0.45.1.obscpio -> melange-0.45.3.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.45.1/go.mod new/melange-0.45.3/go.mod --- old/melange-0.45.1/go.mod 2026-03-09 18:41:30.000000000 +0100 +++ new/melange-0.45.3/go.mod 2026-03-11 00:53:12.000000000 +0100 @@ -22,7 +22,7 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/klauspost/compress v1.18.4 github.com/klauspost/pgzip v1.2.6 - github.com/moby/moby v28.5.2+incompatible + github.com/moby/moby/v2 v2.0.0-beta.7 github.com/opencontainers/image-spec v1.1.1 github.com/package-url/packageurl-go v0.1.4 github.com/pkg/errors v0.9.1 @@ -96,11 +96,11 @@ github.com/charmbracelet/x/term v0.2.1 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect - github.com/containerd/containerd/v2 v2.1.5 // indirect + github.com/containerd/containerd/v2 v2.2.1 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect - github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/cyphar/filepath-securejoin v0.6.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect @@ -122,7 +122,7 @@ github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect github.com/googleapis/gax-go/v2 v2.17.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -168,7 +168,7 @@ go.opentelemetry.io/otel/trace v1.41.0 // indirect go.step.sm/crypto v0.76.2 // indirect golang.org/x/mod v0.33.0 // indirect - golang.org/x/net v0.50.0 // indirect + golang.org/x/net v0.51.0 // indirect golang.org/x/oauth2 v0.35.0 // indirect google.golang.org/api v0.269.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.45.1/go.sum new/melange-0.45.3/go.sum --- old/melange-0.45.1/go.sum 2026-03-09 18:41:30.000000000 +0100 +++ new/melange-0.45.3/go.sum 2026-03-11 00:53:12.000000000 +0100 @@ -35,8 +35,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -68,8 +68,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= -github.com/containerd/containerd/v2 v2.1.5 h1:pWSmPxUszaLZKQPvOx27iD4iH+aM6o0BoN9+hg77cro= -github.com/containerd/containerd/v2 v2.1.5/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM= +github.com/containerd/containerd/v2 v2.2.1 h1:TpyxcY4AL5A+07dxETevunVS5zxqzuq7ZqJXknM11yk= +github.com/containerd/containerd/v2 v2.2.1/go.mod h1:NR70yW1iDxe84F2iFWbR9xfAN0N2F0NcjTi1OVth4nU= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -83,8 +83,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= -github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= -github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is= +github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -196,8 +196,8 @@ github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= @@ -251,16 +251,16 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/moby v28.5.2+incompatible h1:hIn6qcenb3JY1E3STwqEbBvJ8bha+u1LpqjX4CBvNCk= -github.com/moby/moby v28.5.2+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= +github.com/moby/moby/v2 v2.0.0-beta.7 h1:f+Sc06mGJ5eBskZlSEb8+2MgVPTn74Y6xG04xS3VNLw= +github.com/moby/moby/v2 v2.0.0-beta.7/go.mod h1:rKqTDjHJippfr+QpXtKL8G0tZDDc7vanyxaVBL43o8s= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= +github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -368,10 +368,10 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.41.0 h1:61oRQmYGMW7pXmFjPg1Muy84ndqMxQ6SH2L8fBG8fSY= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.41.0/go.mod h1:c0z2ubK4RQL+kSDuuFu9WnuXimObon3IiKjJf4NACvU= go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= @@ -382,8 +382,8 @@ go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= -go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= -go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.step.sm/crypto v0.76.2 h1:JJ/yMcs/rmcCAwlo+afrHjq74XBFRTJw5B2y4Q4Z4c4= go.step.sm/crypto v0.76.2/go.mod h1:m6KlB/HzIuGFep0UWI5e0SYi38UxpoKeCg6qUaHV6/Q= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -421,8 +421,8 @@ 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.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.45.1/pkg/container/bubblewrap_runner.go new/melange-0.45.3/pkg/container/bubblewrap_runner.go --- old/melange-0.45.1/pkg/container/bubblewrap_runner.go 2026-03-09 18:41:30.000000000 +0100 +++ new/melange-0.45.3/pkg/container/bubblewrap_runner.go 2026-03-11 00:53:12.000000000 +0100 @@ -34,7 +34,7 @@ apko_types "chainguard.dev/apko/pkg/build/types" "github.com/chainguard-dev/clog" v1 "github.com/google/go-containerregistry/pkg/v1" - moby "github.com/moby/moby/oci/caps" + moby "github.com/moby/moby/v2/daemon/pkg/oci/caps" "go.opentelemetry.io/otel" ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.45.1/pkg/container/qemu_runner.go new/melange-0.45.3/pkg/container/qemu_runner.go --- old/melange-0.45.1/pkg/container/qemu_runner.go 2026-03-09 18:41:30.000000000 +0100 +++ new/melange-0.45.3/pkg/container/qemu_runner.go 2026-03-11 00:53:12.000000000 +0100 @@ -68,6 +68,20 @@ const ( defaultDiskSize = "50Gi" + + // sshDialTimeout is the maximum time allowed for TCP connect + SSH handshake. + sshDialTimeout = 30 * time.Second + + // sshKeepaliveInterval is how often keepalive requests are sent. + sshKeepaliveInterval = 30 * time.Second + + // sshKeepaliveMaxMissed is the number of consecutive missed keepalives + // before the connection is closed (equivalent to ServerAliveCountMax). + sshKeepaliveMaxMissed = 3 + + // sshKeepaliveRequestTimeout is how long to wait for a single keepalive + // response before counting it as missed. + sshKeepaliveRequestTimeout = 10 * time.Second ) type qemu struct{} @@ -185,7 +199,7 @@ } // Connect to the SSH server - client, err := ssh.Dial("tcp", cfg.SSHAddress, config) + client, err := sshDialWithTimeout(cfg.SSHAddress, config, sshDialTimeout) if err != nil { clog.FromContext(ctx).Errorf("Failed to dial: %s", err) return err @@ -742,7 +756,12 @@ baseargs = append(baseargs, serialArgs...) // use -netdev + -device instead of -nic, as this is better supported by microvm machine type baseargs = append(baseargs, "-netdev", "user,id=id1,hostfwd=tcp:"+cfg.SSHAddress+"-:22,hostfwd=tcp:"+cfg.SSHControlAddress+"-:2223") - baseargs = append(baseargs, "-device", "virtio-net-pci,netdev=id1,romfile=") + // Set host_mtu to avoid silent packet drops in nested environments (e.g., + // QEMU inside GKE pods). SLIRP defaults to 1500 MTU but the host path MTU + // may be lower due to encapsulation (GCP VPC uses 1460, pod networks can be + // lower). The guest kernel picks up host_mtu via virtio feature negotiation + // and configures the interface MTU automatically at boot. + baseargs = append(baseargs, "-device", "virtio-net-pci,netdev=id1,romfile=,host_mtu=1400") // add random generator via pci, improve ssh startup time baseargs = append(baseargs, "-device", "virtio-rng-pci,rng=rng0", "-object", "rng-random,filename=/dev/urandom,id=rng0") // panic=-1 ensures that if the init fails, we immediately exit the machine @@ -931,8 +950,6 @@ select { case <-started: log.Info("qemu: VM started successfully, SSH server is up") - secureDelete(ctx, cfg.InitramfsPath) - cfg.InitramfsPath = "" case err := <-qemuExit: defer os.Remove(cfg.ImgRef) defer os.Remove(cfg.Disk) @@ -950,16 +967,17 @@ return fmt.Errorf("qemu: context canceled while waiting for VM to start: %w", context.Cause(ctx)) } - err = getHostKey(ctx, cfg) - if err != nil { - return fmt.Errorf("qemu: could not get VM host key") + if err := getHostKey(ctx, cfg); err != nil { + return fmt.Errorf("qemu: could not get VM host key: %w", err) } - err = setupSSHClients(ctx, cfg) - if err != nil { - return fmt.Errorf("qemu: could not setup SSH client") + if err := setupSSHClients(ctx, cfg); err != nil { + return fmt.Errorf("qemu: could not setup SSH client: %w", err) } + secureDelete(ctx, cfg.InitramfsPath) + cfg.InitramfsPath = "" + // Zero out sensitive private key material now that all SSH connections are established // The public key is retained for verification in setupSSHClients zeroSensitiveFields(ctx, cfg) @@ -970,7 +988,7 @@ kv, err := getGuestKernelVersion(ctx, cfg) if err != nil { - return fmt.Errorf("qemu: unable to query guest kernel version") + return fmt.Errorf("qemu: unable to query guest kernel version: %w", err) } clog.FromContext(ctx).Infof("qemu: running kernel version: %s", kv) @@ -1141,29 +1159,125 @@ } // Connect to the SSH server - cfg.SSHBuildClient, err = ssh.Dial("tcp", cfg.SSHAddress, buildConfig) + cfg.SSHBuildClient, err = sshDialWithTimeout(cfg.SSHAddress, buildConfig, sshDialTimeout) if err != nil { clog.FromContext(ctx).Errorf("Failed to dial: %s", err) return err } // Connect to the SSH server on the control (unchrooted) port - cfg.SSHControlClient, err = ssh.Dial("tcp", cfg.SSHControlAddress, controlConfig) + cfg.SSHControlClient, err = sshDialWithTimeout(cfg.SSHControlAddress, controlConfig, sshDialTimeout) if err != nil { clog.FromContext(ctx).Errorf("Failed to dial: %s", err) return err } // Connect to the SSH server on the chrooted port with privilege - cfg.SSHControlBuildClient, err = ssh.Dial("tcp", cfg.SSHAddress, controlConfig) + cfg.SSHControlBuildClient, err = sshDialWithTimeout(cfg.SSHAddress, controlConfig, sshDialTimeout) if err != nil { clog.FromContext(ctx).Errorf("Failed to dial: %s", err) return err } + // Start SSH keepalives for all clients to prevent idle disconnects + startSSHKeepalive(ctx, cfg.SSHBuildClient, "build") + startSSHKeepalive(ctx, cfg.SSHControlClient, "control") + startSSHKeepalive(ctx, cfg.SSHControlBuildClient, "control-build") + return nil } +// sshDialWithTimeout dials an SSH connection with a deadline covering both the +// TCP connect and the SSH handshake. This prevents hangs when the remote end +// accepts the TCP connection but stalls during the handshake (golang/go#19338). +func sshDialWithTimeout(addr string, config *ssh.ClientConfig, timeout time.Duration) (*ssh.Client, error) { + conn, err := net.DialTimeout("tcp", addr, timeout) + if err != nil { + return nil, err + } + + // Set a deadline covering the entire SSH handshake + if err := conn.SetDeadline(time.Now().Add(timeout)); err != nil { + conn.Close() + return nil, err + } + + c, chans, reqs, err := ssh.NewClientConn(conn, addr, config) + if err != nil { + conn.Close() + return nil, err + } + + // Clear the deadline so it doesn't affect future operations + if err := conn.SetDeadline(time.Time{}); err != nil { + c.Close() + return nil, err + } + + return ssh.NewClient(c, chans, reqs), nil +} + +// sshKeepaliveClient is the interface needed for SSH keepalive operations. +type sshKeepaliveClient interface { + SendRequest(name string, wantReply bool, payload []byte) (bool, []byte, error) + Close() error +} + +// startSSHKeepalive sends periodic keepalive requests to an SSH client to +// prevent idle connection timeouts. After sshKeepaliveMaxMissed consecutive +// missed keepalives (equivalent to ServerAliveCountMax), the client connection +// is closed. The goroutine stops when the context is canceled or the connection +// is closed. +func startSSHKeepalive(ctx context.Context, client sshKeepaliveClient, name string) { + go runSSHKeepalive(ctx, client, name) +} + +// runSSHKeepalive is the keepalive loop, separated from startSSHKeepalive for +// testability (synctest requires the goroutine to be started inside the bubble). +func runSSHKeepalive(ctx context.Context, client sshKeepaliveClient, name string) { + t := time.NewTicker(sshKeepaliveInterval) + defer t.Stop() + + missed := 0 + for { + select { + case <-ctx.Done(): + return + case <-t.C: + if err := sshSendKeepalive(ctx, client); err != nil { + missed++ + clog.FromContext(ctx).Debugf("ssh keepalive missed for %s client (%d/%d): %v", name, missed, sshKeepaliveMaxMissed, err) + if missed >= sshKeepaliveMaxMissed { + clog.FromContext(ctx).Warnf("ssh keepalive: closing %s client after %d consecutive missed keepalives", name, missed) + client.Close() + return + } + continue + } + missed = 0 + } + } +} + +// sshSendKeepalive sends a single keepalive request with a timeout to prevent +// blocking indefinitely on a hung connection. +func sshSendKeepalive(ctx context.Context, client sshKeepaliveClient) error { + done := make(chan error, 1) + go func() { + _, _, err := client.SendRequest("[email protected]", true, nil) + done <- err + }() + + select { + case err := <-done: + return err + case <-time.After(sshKeepaliveRequestTimeout): + return fmt.Errorf("keepalive request timed out after %s", sshKeepaliveRequestTimeout) + case <-ctx.Done(): + return ctx.Err() + } +} + // getWorkspaceLicenseFiles returns a list of possible license files from the // workspace func getWorkspaceLicenseFiles(ctx context.Context, cfg *Config, extraFiles []string) ([]string, error) { @@ -1285,7 +1399,7 @@ kernel = kernelVar } } else if _, err := os.Stat(kernel); err != nil { - return "", fmt.Errorf("qemu: /boot/vmlinuz not found, specify a kernel path with env variable QEMU_KERNEL_IMAGE") + return "", fmt.Errorf("qemu: /boot/vmlinuz not found, specify a kernel path with env variable QEMU_KERNEL_IMAGE: %w", err) } return kernel, nil @@ -1390,7 +1504,7 @@ HostKeyCallback: ssh.FixedHostKey(cfg.VMHostKeyPublic), } - client, err := ssh.Dial("tcp", cfg.SSHAddress, config) + client, err := sshDialWithTimeout(cfg.SSHAddress, config, sshDialTimeout) if err != nil { clog.FromContext(ctx).Errorf("Failed to verify and connect to VM: %s", err) return fmt.Errorf("vm host key verification failed (possible security issue): %w", err) @@ -1758,7 +1872,7 @@ func randomPortN() (int, error) { l, err := net.Listen("tcp", "localhost:0") if err != nil { - return 0, fmt.Errorf("no open port found") + return 0, fmt.Errorf("no open port found: %w", err) } defer l.Close() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.45.1/pkg/container/qemu_ssh_test.go new/melange-0.45.3/pkg/container/qemu_ssh_test.go --- old/melange-0.45.1/pkg/container/qemu_ssh_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/melange-0.45.3/pkg/container/qemu_ssh_test.go 2026-03-11 00:53:12.000000000 +0100 @@ -0,0 +1,438 @@ +// Copyright 2026 Chainguard, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package container + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "errors" + "net" + "sync/atomic" + "testing" + "testing/synctest" + "time" + + "golang.org/x/crypto/ssh" +) + +// mockKeepaliveClient implements sshKeepaliveClient for testing. +type mockKeepaliveClient struct { + sendFunc func() error + closed atomic.Bool + closeCalled atomic.Int32 +} + +func (m *mockKeepaliveClient) SendRequest(name string, wantReply bool, payload []byte) (bool, []byte, error) { + if m.closed.Load() { + return false, nil, errors.New("client closed") + } + if m.sendFunc != nil { + return false, nil, m.sendFunc() + } + return true, nil, nil +} + +func (m *mockKeepaliveClient) Close() error { + m.closed.Store(true) + m.closeCalled.Add(1) + return nil +} + +// newTestSSHServer starts an in-process SSH server that accepts connections and +// completes the handshake. Returns the listener address and a cleanup function. +func newTestSSHServer(t *testing.T) (string, func()) { + t.Helper() + + hostKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("failed to generate host key: %v", err) + } + hostSigner, err := ssh.NewSignerFromKey(hostKey) + if err != nil { + t.Fatalf("failed to create signer: %v", err) + } + + serverConfig := &ssh.ServerConfig{ + NoClientAuth: true, + } + serverConfig.AddHostKey(hostSigner) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + + done := make(chan struct{}) + go func() { + defer close(done) + for { + conn, err := listener.Accept() + if err != nil { + return + } + go func(c net.Conn) { + defer c.Close() + _, chans, reqs, err := ssh.NewServerConn(c, serverConfig) + if err != nil { + return + } + go ssh.DiscardRequests(reqs) + for newChan := range chans { + newChan.Reject(ssh.Prohibited, "no channels allowed") + } + }(conn) + } + }() + + cleanup := func() { + listener.Close() + <-done + } + + return listener.Addr().String(), cleanup +} + +func TestSSHDialWithTimeout_Success(t *testing.T) { + addr, cleanup := newTestSSHServer(t) + defer cleanup() + + config := &ssh.ClientConfig{ + User: "test", + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + + client, err := sshDialWithTimeout(addr, config, 5*time.Second) + if err != nil { + t.Fatalf("sshDialWithTimeout failed: %v", err) + } + defer client.Close() +} + +func TestSSHDialWithTimeout_ConnectRefused(t *testing.T) { + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + addr := l.Addr().String() + l.Close() + + config := &ssh.ClientConfig{ + User: "test", + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + + _, err = sshDialWithTimeout(addr, config, 2*time.Second) + if err == nil { + t.Fatal("expected error dialing closed port") + } +} + +func TestSSHDialWithTimeout_HandshakeHang(t *testing.T) { + // TCP listener that accepts but never speaks SSH + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer listener.Close() + + go func() { + for { + conn, err := listener.Accept() + if err != nil { + return + } + go func(c net.Conn) { + buf := make([]byte, 1024) + for { + if _, err := c.Read(buf); err != nil { + return + } + } + }(conn) + } + }() + + config := &ssh.ClientConfig{ + User: "test", + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + + timeout := 500 * time.Millisecond + start := time.Now() + _, err = sshDialWithTimeout(listener.Addr().String(), config, timeout) + elapsed := time.Since(start) + + if err == nil { + t.Fatal("expected error on handshake hang") + } + if elapsed > 5*time.Second { + t.Errorf("sshDialWithTimeout took %v, expected ~%v", elapsed, timeout) + } +} + +func TestSSHDialWithTimeout_ReturnsClient(t *testing.T) { + addr, cleanup := newTestSSHServer(t) + defer cleanup() + + config := &ssh.ClientConfig{ + User: "test", + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + + client, err := sshDialWithTimeout(addr, config, 5*time.Second) + if err != nil { + t.Fatalf("dial failed: %v", err) + } + defer client.Close() + + // Session open will fail (server rejects channels) but proves client works + _, err = client.NewSession() + if err == nil { + t.Fatal("expected session rejection from test server") + } +} + +func TestSSHSendKeepalive_Success(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + mock := &mockKeepaliveClient{} + ctx := t.Context() + + if err := sshSendKeepalive(ctx, mock); err != nil { + t.Fatalf("sshSendKeepalive failed on healthy client: %v", err) + } + }) +} + +func TestSSHSendKeepalive_Error(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + mock := &mockKeepaliveClient{ + sendFunc: func() error { + return errors.New("connection reset") + }, + } + ctx := t.Context() + + if err := sshSendKeepalive(ctx, mock); err == nil { + t.Fatal("expected error from failing client") + } + }) +} + +func TestSSHSendKeepalive_Timeout(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + // SendRequest blocks until released, simulating a hung connection + block := make(chan struct{}) + mock := &mockKeepaliveClient{ + sendFunc: func() error { + <-block + return errors.New("unblocked") + }, + } + ctx := t.Context() + + err := sshSendKeepalive(ctx, mock) + if err == nil { + t.Fatal("expected timeout error") + } + t.Logf("got expected error: %v", err) + + // Unblock the leaked goroutine so synctest can clean up + close(block) + synctest.Wait() + }) +} + +func TestSSHSendKeepalive_ContextCanceled(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + // SendRequest blocks until released + block := make(chan struct{}) + mock := &mockKeepaliveClient{ + sendFunc: func() error { + <-block + return errors.New("unblocked") + }, + } + ctx, cancel := context.WithCancel(t.Context()) + + // Cancel after 1s (before the 10s request timeout) + go func() { + time.Sleep(1 * time.Second) + cancel() + }() + + err := sshSendKeepalive(ctx, mock) + if !errors.Is(err, context.Canceled) { + t.Fatalf("expected context.Canceled, got: %v", err) + } + + // Unblock the leaked goroutine so synctest can clean up + close(block) + synctest.Wait() + }) +} + +func TestRunSSHKeepalive_SendsAtInterval(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + var count atomic.Int32 + mock := &mockKeepaliveClient{ + sendFunc: func() error { + count.Add(1) + return nil + }, + } + + ctx, cancel := context.WithCancel(t.Context()) + go runSSHKeepalive(ctx, mock, "test") + + // Advance past 3 intervals (30s each = 90s) + time.Sleep(sshKeepaliveInterval*3 + time.Nanosecond) + synctest.Wait() + + got := count.Load() + if got != 3 { + t.Errorf("expected 3 keepalives after 3 intervals, got %d", got) + } + + cancel() + synctest.Wait() + }) +} + +func TestRunSSHKeepalive_ClosesAfterMaxMissed(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + mock := &mockKeepaliveClient{ + sendFunc: func() error { + return errors.New("connection reset") + }, + } + + go runSSHKeepalive(t.Context(), mock, "test") + + // After sshKeepaliveMaxMissed intervals, the client should be closed. + // Each tick is 30s; need 3 ticks for 3 misses. + time.Sleep(sshKeepaliveInterval*time.Duration(sshKeepaliveMaxMissed) + time.Nanosecond) + synctest.Wait() + + if !mock.closed.Load() { + t.Fatal("expected client to be closed after max missed keepalives") + } + if got := mock.closeCalled.Load(); got != 1 { + t.Errorf("expected Close called once, got %d", got) + } + }) +} + +func TestRunSSHKeepalive_ResetsOnSuccess(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + var callCount atomic.Int32 + mock := &mockKeepaliveClient{ + sendFunc: func() error { + n := callCount.Add(1) + // Fail on calls 1 and 2, succeed on 3, fail on 4 and 5, succeed on 6... + if n%3 != 0 { + return errors.New("temporary failure") + } + return nil + }, + } + + ctx, cancel := context.WithCancel(t.Context()) + go runSSHKeepalive(ctx, mock, "test") + + // After 6 intervals: fail, fail, succeed, fail, fail, succeed + // The missed counter resets each time a keepalive succeeds, + // so we never hit maxMissed (3). + time.Sleep(sshKeepaliveInterval*6 + time.Nanosecond) + synctest.Wait() + + if mock.closed.Load() { + t.Fatal("client should not be closed — missed counter resets on success") + } + + cancel() + synctest.Wait() + }) +} + +func TestRunSSHKeepalive_StopsOnContextCancel(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + var count atomic.Int32 + mock := &mockKeepaliveClient{ + sendFunc: func() error { + count.Add(1) + return nil + }, + } + + ctx, cancel := context.WithCancel(t.Context()) + go runSSHKeepalive(ctx, mock, "test") + + // Let one keepalive fire + time.Sleep(sshKeepaliveInterval + time.Nanosecond) + synctest.Wait() + + if got := count.Load(); got != 1 { + t.Fatalf("expected 1 keepalive before cancel, got %d", got) + } + + // Cancel and verify no more keepalives fire + cancel() + synctest.Wait() + + countAfterCancel := count.Load() + time.Sleep(sshKeepaliveInterval * 3) + synctest.Wait() + + if got := count.Load(); got != countAfterCancel { + t.Errorf("keepalives continued after cancel: had %d, now %d", countAfterCancel, got) + } + + if mock.closed.Load() { + t.Error("client should not be closed on context cancel") + } + }) +} + +func TestRunSSHKeepalive_HungRequestCountsAsMiss(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + // SendRequest blocks until released — each keepalive should time out + // after sshKeepaliveRequestTimeout and count as a miss. + block := make(chan struct{}) + mock := &mockKeepaliveClient{ + sendFunc: func() error { + <-block + return errors.New("unblocked") + }, + } + + go runSSHKeepalive(t.Context(), mock, "test") + + // Each keepalive tick (30s) starts a request that times out after 10s, + // so each tick takes ~30s from the ticker's perspective (the timeout + // completes within the interval). After 3 ticks, client should close. + time.Sleep(sshKeepaliveInterval*time.Duration(sshKeepaliveMaxMissed) + sshKeepaliveRequestTimeout + time.Nanosecond) + synctest.Wait() + + if !mock.closed.Load() { + t.Fatal("expected client to be closed after hung requests exceed max missed") + } + + // Unblock leaked goroutines so synctest can clean up + close(block) + synctest.Wait() + }) +} ++++++ melange.obsinfo ++++++ --- /var/tmp/diff_new_pack.Sh5Z5m/_old 2026-03-11 20:57:28.987277840 +0100 +++ /var/tmp/diff_new_pack.Sh5Z5m/_new 2026-03-11 20:57:29.031279655 +0100 @@ -1,5 +1,5 @@ name: melange -version: 0.45.1 -mtime: 1773078090 -commit: 049664c63b543602ba8e344d2ebf0bdd950f3428 +version: 0.45.3 +mtime: 1773186792 +commit: 2a226faac364a05b188a3afcbbdc45b35b395ff3 ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/melange/vendor.tar.gz /work/SRC/openSUSE:Factory/.melange.new.8177/vendor.tar.gz differ: char 133, line 1
