Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package melange for openSUSE:Factory checked in at 2025-04-25 22:19:12 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/melange (Old) and /work/SRC/openSUSE:Factory/.melange.new.30101 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "melange" Fri Apr 25 22:19:12 2025 rev:79 rq:1272568 version:0.23.10 Changes: -------- --- /work/SRC/openSUSE:Factory/melange/melange.changes 2025-04-20 20:03:33.522049848 +0200 +++ /work/SRC/openSUSE:Factory/.melange.new.30101/melange.changes 2025-04-25 22:20:18.118039464 +0200 @@ -1,0 +2,24 @@ +Fri Apr 25 06:24:15 UTC 2025 - Johannes Kastl <opensuse_buildserv...@ojkastl.de> + +- Update to version 0.23.10: + * Use virtconsole for stdout, and serial in microvm. (#1947) + * Add config/env var support for microVM usage; display boot logs + in debug (#1945) + * Re-add xattr allowlist from readlinkFS (#1942) + * Update declarative capabilities; add functionality to set + within QEMU (#1944) + * Revert "fix: propagate Range field of subpackages (#1939)" + (#1941) + +------------------------------------------------------------------- +Thu Apr 24 15:31:50 UTC 2025 - Johannes Kastl <opensuse_buildserv...@ojkastl.de> + +- Update to version 0.23.9: + * If the install location is customized at all, these find + commands will fail and cause the build to exit. Wrapping it in + a validifity check is a simple way to prevent this from failing + outright. (#1943) + * Add support for declarative file capabilities (#1938) + * fix: propagate Range field of subpackages (#1939) + +------------------------------------------------------------------- Old: ---- melange-0.23.8.obscpio New: ---- melange-0.23.10.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ melange.spec ++++++ --- /var/tmp/diff_new_pack.Kcxvte/_old 2025-04-25 22:20:18.838069724 +0200 +++ /var/tmp/diff_new_pack.Kcxvte/_new 2025-04-25 22:20:18.842069892 +0200 @@ -17,7 +17,7 @@ Name: melange -Version: 0.23.8 +Version: 0.23.10 Release: 0 Summary: Build APKs from source code License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.Kcxvte/_old 2025-04-25 22:20:18.882071574 +0200 +++ /var/tmp/diff_new_pack.Kcxvte/_new 2025-04-25 22:20:18.882071574 +0200 @@ -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.23.8</param> + <param name="revision">v0.23.10</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.Kcxvte/_old 2025-04-25 22:20:18.902072414 +0200 +++ /var/tmp/diff_new_pack.Kcxvte/_new 2025-04-25 22:20:18.906072582 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/chainguard-dev/melange</param> - <param name="changesrevision">ef048762555067c144b90f3f9acdd286249acbee</param></service></servicedata> + <param name="changesrevision">86fd5c775314025bef9fa0a5f9dee8ded4c6c10a</param></service></servicedata> (No newline at EOF) ++++++ melange-0.23.8.obscpio -> melange-0.23.10.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.23.8/pkg/build/build.go new/melange-0.23.10/pkg/build/build.go --- old/melange-0.23.8/pkg/build/build.go 2025-04-18 18:38:05.000000000 +0200 +++ new/melange-0.23.10/pkg/build/build.go 2025-04-24 23:52:12.000000000 +0200 @@ -18,6 +18,7 @@ "archive/tar" "compress/gzip" "context" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -313,6 +314,7 @@ b.ExtraPackages = append(b.ExtraPackages, []string{ "melange-microvm-init", "gnutar", + "attr", }...) } @@ -977,6 +979,34 @@ } } + // For each `setcap` entry in the package/sub-package, pull out the capability and data and set the xattr + // For example: + // setcap: + // - path: /usr/bin/scary + // add: + // cap_sys_admin: "+ep" + caps, err := config.ParseCapabilities(b.Configuration.Package.SetCap) + if err != nil { + log.Warnf("failed to collect encoded capabilities for %v: %v", b.Configuration.Package.SetCap, err) + } + + for path, cap := range caps { + enc := config.EncodeCapability(cap.Effective, cap.Permitted, cap.Inheritable) + fullPath := filepath.Join(melangeOutputDirName, pkg.Name, path) + if b.Runner.Name() == container.QemuName { + fullPath := filepath.Join(WorkDir, melangeOutputDirName, pkg.Name, path) + hex := fmt.Sprintf("0x%s", hex.EncodeToString(enc)) + cmd := []string{"/bin/sh", "-c", fmt.Sprintf("setfattr -n security.capability -v %s %s", hex, fullPath)} + if err := b.Runner.Run(ctx, pr.config, map[string]string{}, cmd...); err != nil { + return fmt.Errorf("failed to set capabilities within VM on %s: %v\n", path, err) + } + } else { + if err := b.WorkspaceDirFS.SetXattr(fullPath, "security.capability", enc); err != nil { + log.Warnf("failed to set capabilities on %s: %v\n", path, err) + } + } + } + if err := b.retrieveWorkspace(ctx, b.WorkspaceDirFS); err != nil { return fmt.Errorf("retrieving workspace: %w", err) } @@ -1221,6 +1251,7 @@ cfg.CPUModel = b.Configuration.Package.Resources.CPUModel cfg.Memory = b.Configuration.Package.Resources.Memory cfg.Disk = b.Configuration.Package.Resources.Disk + cfg.MicroVM = b.Configuration.Package.Resources.MicroVM } if b.Configuration.Capabilities.Add != nil { cfg.Capabilities.Add = b.Configuration.Capabilities.Add @@ -1360,6 +1391,17 @@ return time.Unix(sec, 0).UTC(), nil } +// xattrIgnoreList contains a mapping of xattr names used by various +// security features which leak their state into packages. We need to +// ignore these xattrs because they require special permissions to be +// set when the underlying security features are in use. +var xattrIgnoreList = map[string]bool{ + "com.apple.provenance": true, + "security.csm": true, + "security.selinux": true, + "com.docker.grpcfuse.ownership": true, +} + // Record on-disk xattrs and mode bits set during package builds in order to apply them in the new in-memory filesystem // This will allow in-memory and bind mount runners to persist xattrs correctly func storeXattrs(dir string) (map[string]map[string][]byte, map[string]fs.FileMode, error) { @@ -1404,6 +1446,10 @@ attrs := stringsFromByteSlice(buf[:read]) result := make(map[string][]byte) for _, attr := range attrs { + if _, ok := xattrIgnoreList[attr]; ok { + continue + } + s, err := unix.Getxattr(path, attr, nil) if err != nil { continue diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.23.8/pkg/build/pipelines/ruby/clean.yaml new/melange-0.23.10/pkg/build/pipelines/ruby/clean.yaml --- old/melange-0.23.8/pkg/build/pipelines/ruby/clean.yaml 2025-04-18 18:38:05.000000000 +0200 +++ new/melange-0.23.10/pkg/build/pipelines/ruby/clean.yaml 2025-04-24 23:52:12.000000000 +0200 @@ -15,5 +15,7 @@ INSTALL_DIR=${{targets.contextdir}}/$(ruby -e 'puts Gem.default_dir') rm -rf ${INSTALL_DIR}/build_info \ ${INSTALL_DIR}/cache - find ${INSTALL_DIR} -name 'gem_make.out' -exec rm {} \; - find ${INSTALL_DIR} -name 'mkmf.log' -exec rm {} \; + if [ -d "${INSTALL_DIR}" ]; then + find "${INSTALL_DIR}" -name 'gem_make.out' -exec rm {} \; + find "${INSTALL_DIR}" -name 'mkmf.log' -exec rm {} \; + fi diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.23.8/pkg/config/config.go new/melange-0.23.10/pkg/config/config.go --- old/melange-0.23.8/pkg/config/config.go 2025-04-18 18:38:05.000000000 +0200 +++ new/melange-0.23.10/pkg/config/config.go 2025-04-24 23:52:12.000000000 +0200 @@ -17,6 +17,7 @@ import ( "bytes" "context" + "encoding/binary" "errors" "fmt" "io/fs" @@ -125,6 +126,8 @@ // The CPE field values to be used for matching against NVD vulnerability // records, if known. CPE CPE `json:"cpe,omitempty" yaml:"cpe,omitempty"` + // Capabilities to set after the pipeline completes. + SetCap []Capability `json:"setcap,omitempty" yaml:"setcap,omitempty"` // Optional: The amount of time to allow this build to take before timing out. Timeout time.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"` @@ -155,6 +158,15 @@ Other string `json:"other,omitempty" yaml:"other,omitempty"` } +// Capability stores paths and an associated map of capabilities and justification to include in a package. +// These capabilities will be set after pipelines run to avoid permissions issues with `setcap`. +// Empty justifications will result in an error. +type Capability struct { + Path string `json:"path,omitempty" yaml:"path,omitempty"` + Add map[string]string `json:"add,omitempty" yaml:"add,omitempty"` + Reason string `json:"reason,omitempty" yaml:"reason,omitempty"` +} + func (cpe CPE) IsZero() bool { return cpe == CPE{} } @@ -164,6 +176,7 @@ CPUModel string `json:"cpumodel,omitempty" yaml:"cpumodel,omitempty"` Memory string `json:"memory,omitempty" yaml:"memory,omitempty"` Disk string `json:"disk,omitempty" yaml:"disk,omitempty"` + MicroVM bool `json:"microvm,omitempty" yaml:"microvm,omitempty"` } // CPEString returns the CPE string for the package, suitable for matching @@ -694,6 +707,8 @@ Checks Checks `json:"checks,omitempty" yaml:"checks,omitempty"` // Test section for the subpackage. Test *Test `json:"test,omitempty" yaml:"test,omitempty"` + // Capabilities to set after the pipeline completes. + SetCap []Capability `json:"setcap,omitempty" yaml:"setcap,omitempty"` } type Input struct { @@ -1277,6 +1292,7 @@ CPE: in.CPE, Timeout: in.Timeout, Resources: in.Resources, + SetCap: in.SetCap, } } @@ -1630,6 +1646,9 @@ if err := validatePipelines(ctx, cfg.Pipeline); err != nil { return ErrInvalidConfiguration{Problem: err} } + if err := validateCapabilities(cfg.Package.SetCap); err != nil { + return ErrInvalidConfiguration{Problem: err} + } saw := map[string]int{cfg.Package.Name: -1} for i, sp := range cfg.Subpackages { @@ -1656,6 +1675,9 @@ if err := validatePipelines(ctx, sp.Pipeline); err != nil { return ErrInvalidConfiguration{Problem: err} } + if err := validateCapabilities(sp.SetCap); err != nil { + return ErrInvalidConfiguration{Problem: err} + } } if err := validateCPE(cfg.Package.CPE); err != nil { @@ -1795,3 +1817,126 @@ } } } + +// validCapabilities contains a list of _in-use_ capabilities and their respective bits from existing package specs. +// https://github.com/torvalds/linux/blob/master/include/uapi/linux/capability.h#L106-L422 +var validCapabilities = map[string]uint32{ + "cap_net_bind_service": 10, + "cap_net_admin": 12, + "cap_net_raw": 13, + "cap_ipc_lock": 14, + "cap_sys_admin": 21, +} + +func getCapabilityValue(attr string) uint32 { + if value, ok := validCapabilities[attr]; ok { + return 1 << value + } + return 0 +} + +func validateCapabilities(setcap []Capability) error { + var errs []error + + for _, cap := range setcap { + for add := range cap.Add { + // Allow for multiple capabilities per addition + // e.g., cap_net_raw,cap_net_admin,cap_net_bind_service+eip + for p := range strings.SplitSeq(add, ",") { + if _, ok := validCapabilities[p]; !ok { + errs = append(errs, fmt.Errorf("invalid capability %q for path %q", p, cap.Path)) + } + } + } + if cap.Reason == "" { + errs = append(errs, fmt.Errorf("unjustified reason for capability %q", cap.Add)) + } + } + + if len(errs) == 0 { + return nil + } + + return errors.Join(errs...) +} + +type capabilityData struct { + Effective uint32 + Permitted uint32 + Inheritable uint32 +} + +// ParseCapabilities processes all capabilities for a given path. +func ParseCapabilities(caps []Capability) (map[string]capabilityData, error) { + pathCapabilities := map[string]capabilityData{} + + for _, c := range caps { + for attrs, data := range c.Add { + for attr := range strings.SplitSeq(attrs, ",") { + capValues := getCapabilityValue(attr) + effective, permitted, inheritable := parseCapability(data) + + caps, ok := pathCapabilities[c.Path] + if !ok { + caps = struct { + Effective uint32 + Permitted uint32 + Inheritable uint32 + }{} + } + + if effective { + caps.Effective |= capValues + } + if permitted { + caps.Permitted |= capValues + } + if inheritable { + caps.Inheritable |= capValues + } + + pathCapabilities[c.Path] = caps + } + } + } + + return pathCapabilities, nil +} + +// parseCapability determines which bits are set for a given capability. +func parseCapability(capFlag string) (effective, permitted, inheritable bool) { + for _, c := range capFlag { + switch c { + case 'e': + effective = true + case 'p': + permitted = true + case 'i': + inheritable = true + } + } + return +} + +// EncodeCapability returns the byte slice necessary to set the final capability xattr. +func EncodeCapability(effectiveBits, permittedBits, inheritableBits uint32) []byte { + revision := uint32(0x03000000) + + var flags uint32 = 0 + if effectiveBits != 0 { + flags = 0x01 + } + magic := revision | flags + + data := make([]byte, 24) + + binary.LittleEndian.PutUint32(data[0:4], magic) + binary.LittleEndian.PutUint32(data[4:8], permittedBits) + binary.LittleEndian.PutUint32(data[8:12], inheritableBits) + + binary.LittleEndian.PutUint32(data[12:16], 0) + binary.LittleEndian.PutUint32(data[16:20], 0) + binary.LittleEndian.PutUint32(data[20:24], 0) + + return data +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.23.8/pkg/config/config_test.go new/melange-0.23.10/pkg/config/config_test.go --- old/melange-0.23.8/pkg/config/config_test.go 2025-04-18 18:38:05.000000000 +0200 +++ new/melange-0.23.10/pkg/config/config_test.go 2025-04-24 23:52:12.000000000 +0200 @@ -1,8 +1,11 @@ package config import ( + "bytes" + "encoding/binary" "os" "path/filepath" + "strings" "testing" "github.com/chainguard-dev/clog/slogtest" @@ -531,3 +534,392 @@ } } } + +func TestSetCap(t *testing.T) { + tests := []struct { + setcap []Capability + err bool + }{ + { + []Capability{ + { + Path: "/bar", + Add: map[string]string{"cap_net_bind_service": "+eip"}, + Reason: "Needed for package foo because xyz", + }, + }, + false, + }, + { + []Capability{ + { + Path: "/bar", + Add: map[string]string{"cap_net_raw": "+eip"}, + Reason: "Needed for package baz because xyz", + }, + }, + false, + }, + { + []Capability{ + { + Path: "/bar", + Add: map[string]string{"cap_net_raw,cap_net_admin,cap_net_bind_service": "+ep"}, + Reason: "Valid combination of three capabilities on a single line", + }, + }, + false, + }, + { + []Capability{ + { + Path: "/bar", + Add: map[string]string{ + "cap_net_raw": "+ep", + "cap_net_admin": "+ep", + "cap_net_bind_service": "+ep", + }, + Reason: "Valid combination of three capabilities on separate lines", + }, + }, + false, + }, + { + []Capability{ + { + Path: "/foo", + Add: map[string]string{ + "cap_net_raw": "+ep", + }, + Reason: "First package in a multi-package, multi-capability capability addition.", + }, + { + Path: "/bar", + Add: map[string]string{ + "cap_net_admin": "+ep", + "cap_net_bind_service": "+ep", + }, + Reason: "Second package in a multi-package, multi-capability capability addition.", + }, + { + Path: "/baz", + Add: map[string]string{ + "cap_net_raw,cap_net_admin,cap_net_bind_service": "+eip", + }, + Reason: "Third package in a multi-package, multi-capability capability addition.", + }, + }, + false, + }, + { + []Capability{ + { + Path: "/foo", + Add: map[string]string{ + "cap_net_raw": "+ep", + }, + Reason: "First package in a multi-package, multi-capability capability addition.", + }, + { + Path: "/bar", + Add: map[string]string{ + "cap_setfcap": "+ep", + "cap_net_bind_service": "+ep", + }, + Reason: "Tying to sneak an invalid capability into multiple paths.", + }, + { + Path: "/baz", + Add: map[string]string{ + "cap_net_raw,cap_net_admin,cap_net_bind_service": "+eip", + }, + Reason: "Third package in a multi-package, multi-capability capability addition.", + }, + }, + true, + }, + { + []Capability{ + { + Path: "/bar", + Add: map[string]string{"cap_sys_admin": "+ep"}, + Reason: "Needed for package baz", + }, + }, + false, + }, + { + []Capability{ + { + Path: "/bar", + Add: map[string]string{"cap_ipc_lock": "+ep"}, + Reason: "Needed for package baz", + }, + }, + false, + }, + { + []Capability{ + { + Path: "/bar", + Add: map[string]string{"cap_net_admin": "+ep"}, + Reason: "Needed for package baz", + }, + }, + false, + }, + { + []Capability{ + { + Path: "/bar", + Add: map[string]string{"cap_net_admin": "+ep"}, + Reason: "", + }, + }, + true, + }, + { + []Capability{ + { + Path: "/bar", + Add: map[string]string{"cap_setfcap": "+ep"}, + Reason: "I want to arbitrarily set capabilities", + }, + }, + true, + }, + } + + for _, test := range tests { + err := validateCapabilities(test.setcap) + if (err != nil) != test.err { + t.Errorf("validateCapabilities(%v) returned error %v, expected error: %v", test.setcap, err, test.err) + } + } +} + +// Mock resources to test setcap capabilities +type mockFS struct { + xattrs map[string]map[string][]byte +} + +func newMockFS() *mockFS { + return &mockFS{ + xattrs: make(map[string]map[string][]byte), + } +} + +func (fs *mockFS) SetXattr(path, attr string, value []byte) error { + if _, ok := fs.xattrs[path]; !ok { + fs.xattrs[path] = make(map[string][]byte) + } + fs.xattrs[path][attr] = value + return nil +} + +func (fs *mockFS) GetXattr(path, attr string) ([]byte, error) { + if attrs, ok := fs.xattrs[path]; ok { + if value, ok := attrs[attr]; ok { + return value, nil + } + } + return nil, os.ErrNotExist +} + +func TestSetCapability(t *testing.T) { + type Config struct { + Package struct { + SetCap []Capability + } + } + + type Builder struct { + Configuration Config + WorkspaceDirFS *mockFS + } + + testCases := []struct { + name string + caps []Capability + expectedAttrs map[string]map[string][]byte + }{ + { + name: "Basic capability +ep", + caps: []Capability{ + { + Path: "/usr/bin/fping", + Add: map[string]string{ + "cap_net_raw": "+ep", + }, + Reason: "foo", + }, + }, + expectedAttrs: map[string]map[string][]byte{ + "/usr/bin/fping": { + "security.capability": nil, + }, + }, + }, + { + name: "Multiple capabilities", + caps: []Capability{ + { + Path: "/usr/bin/traceroute", + Add: map[string]string{ + "cap_net_raw": "+ep", + "cap_net_admin": "+eip", + }, + Reason: "foo", + }, + }, + expectedAttrs: map[string]map[string][]byte{ + "/usr/bin/traceroute": { + "security.capability": nil, + }, + }, + }, + { + name: "Multiple paths", + caps: []Capability{ + { + Path: "/bin/ping", + Add: map[string]string{ + "cap_net_raw": "+ep", + }, + Reason: "foo", + }, + { + Path: "/usr/bin/traceroute", + Add: map[string]string{ + "cap_net_admin": "+eip", + }, + Reason: "foo", + }, + }, + expectedAttrs: map[string]map[string][]byte{ + "/bin/ping": { + "security.capability": nil, + }, + "/usr/bin/traceroute": { + "security.capability": nil, + }, + }, + }, + { + name: "Single-line capabilities with same flags", + caps: []Capability{ + { + Path: "/bin/custom-tool", + Add: map[string]string{ + "cap_net_raw,cap_net_admin": "+ep", + }, + Reason: "foo", + }, + }, + expectedAttrs: map[string]map[string][]byte{ + "/bin/custom-tool": { + "security.capability": nil, + }, + }, + }, + { + name: "Multiple comma-separated capabilities with different flags", + caps: []Capability{ + { + Path: "/usr/bin/privileged-tool", + Add: map[string]string{ + "cap_net_raw,cap_net_admin,cap_net_bind_service": "+eip", + "cap_sys_admin": "+p", + }, + Reason: "foo", + }, + }, + expectedAttrs: map[string]map[string][]byte{ + "/usr/bin/privileged-tool": { + "security.capability": nil, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + b := &Builder{ + WorkspaceDirFS: newMockFS(), + } + b.Configuration.Package.SetCap = tc.caps + + caps, err := ParseCapabilities(b.Configuration.Package.SetCap) + if err != nil { + t.Fatalf("Failed to collect capabilities: %v", err) + } + + expectedAttrs := make(map[string][]byte) + for path, c := range caps { + encoded := EncodeCapability(c.Effective, c.Permitted, c.Inheritable) + expectedAttrs[path] = encoded + + if err := b.WorkspaceDirFS.SetXattr(path, "security.capability", encoded); err != nil { + t.Fatalf("failed to set xattr for %s: %v", path, err) + } + } + + for path, expected := range expectedAttrs { + data, err := b.WorkspaceDirFS.GetXattr(path, "security.capability") + if err != nil { + t.Errorf("Failed to get xattr %s: %v", path, err) + continue + } + + if !bytes.Equal(data, expected) { + t.Errorf("Mismatched xattr for %s:\ngot: %x\nwant: %x", path, data, expected) + } + + if len(data) < 24 { + t.Errorf("Capability data too short for %s: got %d bytes", path, len(data)) + continue + } + + magic := binary.LittleEndian.Uint32(data[0:4]) + revision := magic & 0xFF000000 + flags := magic & 0x000000FF + + if revision != 0x03000000 { + t.Errorf("Invalid revision: %x", revision) + } + + permitted := binary.LittleEndian.Uint32(data[4:8]) + inheritable := binary.LittleEndian.Uint32(data[8:12]) + rootid := binary.LittleEndian.Uint32(data[20:24]) + + if rootid != 0 { + t.Errorf("Unexpected rootid: %d", rootid) + } + + effective := flags & 0x01 + + for _, capEntry := range tc.caps { + if capEntry.Path != path { + continue + } + for attr, flag := range capEntry.Add { + for _, a := range strings.Split(attr, ",") { + val := getCapabilityValue(a) + e, p, i := parseCapability(flag) + + if e && effective != 1 { + t.Errorf("Expected effective bit set for %s", path) + } + if p && (permitted&val != val) { + t.Errorf("Expected permitted cap %s in %s", a, path) + } + if i && (inheritable&val != val) { + t.Errorf("Expected inheritable cap %s in %s", a, path) + } + } + } + } + } + }) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.23.8/pkg/container/config.go new/melange-0.23.10/pkg/container/config.go --- old/melange-0.23.8/pkg/container/config.go 2025-04-18 18:38:05.000000000 +0200 +++ new/melange-0.23.10/pkg/container/config.go 2025-04-24 23:52:12.000000000 +0200 @@ -60,4 +60,5 @@ SSHHostKey string Disk string Timeout time.Duration + MicroVM bool } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.23.8/pkg/container/qemu_runner.go new/melange-0.23.10/pkg/container/qemu_runner.go --- old/melange-0.23.8/pkg/container/qemu_runner.go 2025-04-18 18:38:05.000000000 +0200 +++ new/melange-0.23.10/pkg/container/qemu_runner.go 2025-04-24 23:52:12.000000000 +0200 @@ -477,19 +477,39 @@ baseargs := []string{} microvm := false - // load microvm profile and bios, shave some milliseconds from boot - // using this will make a complete boot->initrd (with working network) In ~700ms - // instead of ~900ms. - for _, p := range []string{ - "/usr/share/qemu/bios-microvm.bin", - "/usr/share/seabios/bios-microvm.bin", - } { - if _, err := os.Stat(p); err == nil && cfg.Arch.ToAPK() != "aarch64" { - // only enable pcie for network, enable RTC for kernel, disable i8254PIT, i8259PIC and serial port - baseargs = append(baseargs, "-machine", "microvm,rtc=on,pcie=on,pit=off,pic=off,isa-serial=off") - baseargs = append(baseargs, "-bios", p) - microvm = true - break + // by default, cfg.MicroVM will be false + // override the MicroVM config value with the environment variable if present + // and said variable is parseable as a bool + if env, ok := os.LookupEnv("QEMU_USE_MICROVM"); ok { + if val, err := strconv.ParseBool(env); err == nil { + cfg.MicroVM = val + } + } + + kernelConsole := "console=hvc0" + serialArgs := []string{ + "-device", "virtio-serial-pci,id=virtio-serial0", + "-chardev", "stdio,id=charconsole0", + "-device", "virtconsole,chardev=charconsole0,id=console0", + } + if cfg.MicroVM { + // load microvm profile and bios, shave some milliseconds from boot + // using this will make a complete boot->initrd (with working network) In ~700ms + // instead of ~900ms. + for _, p := range []string{ + "/usr/share/qemu/bios-microvm.bin", + "/usr/share/seabios/bios-microvm.bin", + } { + if _, err := os.Stat(p); err == nil && cfg.Arch.ToAPK() != "aarch64" { + // only enable pcie for network, enable RTC for kernel, disable i8254PIT, i8259PIC and serial port + baseargs = append(baseargs, "-machine", "microvm,rtc=on,pcie=on,pit=off,pic=off,isa-serial=on") + baseargs = append(baseargs, "-bios", p) + // microvm in qemu any version tested will not send hvc0/virtconsole to stdout + kernelConsole = "console=ttyS0" + serialArgs = []string{"-serial", "stdio"} + microvm = true + break + } } } @@ -561,15 +581,15 @@ } } - baseargs = append(baseargs, "-daemonize") // ensure we disable unneeded devices, this is less needed if we use microvm machines // but still useful otherwise baseargs = append(baseargs, "-display", "none") baseargs = append(baseargs, "-no-reboot") baseargs = append(baseargs, "-no-user-config") + baseargs = append(baseargs, "-nographic") baseargs = append(baseargs, "-nodefaults") baseargs = append(baseargs, "-parallel", "none") - baseargs = append(baseargs, "-serial", "none") + baseargs = append(baseargs, serialArgs...) baseargs = append(baseargs, "-vga", "none") // 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") @@ -579,7 +599,7 @@ // panic=-1 ensures that if the init fails, we immediately exit the machine // Add default SSH keys to the VM sshkey := base64.StdEncoding.EncodeToString(pubKey) - baseargs = append(baseargs, "-append", "quiet nomodeset panic=-1 sshkey="+sshkey) + baseargs = append(baseargs, "-append", kernelConsole+" debug loglevel=7 nomodeset panic=-1 sshkey="+sshkey) // we will *not* mount workspace using qemu, this will use 9pfs which is network-based, and will // kill all performances (lots of small files) // instead we will copy back the finished workspace artifacts when done. @@ -611,41 +631,89 @@ qemuCmd := exec.CommandContext(ctx, fmt.Sprintf("qemu-system-%s", cfg.Arch.ToAPK()), baseargs...) clog.FromContext(ctx).Debugf("qemu: executing - %s", strings.Join(qemuCmd.Args, " ")) - output, err := qemuCmd.CombinedOutput() - if err != nil { - // ensure cleanup of resources + outRead, outWrite := io.Pipe() + errRead, errWrite := io.Pipe() + + qemuCmd.Stdout = outWrite + qemuCmd.Stderr = errWrite + + if err := qemuCmd.Start(); err != nil { defer os.Remove(cfg.ImgRef) defer os.Remove(cfg.Disk) - - clog.FromContext(ctx).Errorf("qemu: failed to run qemu command: %v - %s", err, string(output)) - return err + return fmt.Errorf("qemu: failed to start qemu command: %w", err) } - // 5 minutes timeout - retries := 6000 - try := 0 - for try <= retries { - if err := ctx.Err(); err != nil { - return fmt.Errorf("checking SSH server: %w", err) - } - - try++ - time.Sleep(time.Millisecond * 500) - - clog.FromContext(ctx).Debugf("qemu: waiting for ssh to come up, try %d of %d", try, retries) - // Attempt to connect to the address - err = checkSSHServer(cfg.SSHAddress) - if err == nil { - break - } else { - clog.FromContext(ctx).Debug(err.Error()) + logCtx, cancel := context.WithCancel(ctx) + defer cancel() + + go func() { + defer outRead.Close() + scanner := bufio.NewScanner(outRead) + for scanner.Scan() && logCtx.Err() == nil { + line := scanner.Text() + log.Debugf("qemu: %s", line) + } + if err := scanner.Err(); err != nil { + log.Warnf("qemu stdout scanner error: %v", err) + } + }() + + go func() { + defer errRead.Close() + scanner := bufio.NewScanner(errRead) + for scanner.Scan() && logCtx.Err() == nil { + line := scanner.Text() + log.Warnf("qemu: %s", line) + } + if err := scanner.Err(); err != nil { + log.Warnf("qemu stderr scanner error: %v", err) + } + }() + + qemuExit := make(chan error, 1) + go func() { + err := qemuCmd.Wait() + outWrite.Close() + errWrite.Close() + qemuExit <- err + }() + + started := make(chan struct{}) + + go func() { + // one-hour timeout with a 500ms sleep + retries := 7200 + try := 0 + for try < retries { + if logCtx.Err() != nil { + return + } + + try++ + time.Sleep(time.Millisecond * 500) + + log.Debugf("qemu: waiting for ssh to come up, try %d of %d", try, retries) + err = checkSSHServer(cfg.SSHAddress) + if err == nil { + close(started) + return + } else { + log.Debug(err.Error()) + } } - } - if try >= retries { - // ensure cleanup of resources + }() + + select { + case <-started: + log.Info("qemu: VM started successfully, SSH server is up") + case err := <-qemuExit: + defer os.Remove(cfg.ImgRef) + defer os.Remove(cfg.Disk) + return fmt.Errorf("qemu: VM exited unexpectedly: %v", err) + case <-ctx.Done(): defer os.Remove(cfg.ImgRef) defer os.Remove(cfg.Disk) - return fmt.Errorf("qemu: could not start VM, timeout reached") + return fmt.Errorf("qemu: context canceled while waiting for VM to start") } err = getHostKey(ctx, cfg) ++++++ melange.obsinfo ++++++ --- /var/tmp/diff_new_pack.Kcxvte/_old 2025-04-25 22:20:19.146082668 +0200 +++ /var/tmp/diff_new_pack.Kcxvte/_new 2025-04-25 22:20:19.150082837 +0200 @@ -1,5 +1,5 @@ name: melange -version: 0.23.8 -mtime: 1744994285 -commit: ef048762555067c144b90f3f9acdd286249acbee +version: 0.23.10 +mtime: 1745531532 +commit: 86fd5c775314025bef9fa0a5f9dee8ded4c6c10a ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/melange/vendor.tar.gz /work/SRC/openSUSE:Factory/.melange.new.30101/vendor.tar.gz differ: char 115, line 1