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

Reply via email to