Repository: lima-vm/lima

Issue:
Consider setting `CONFIG_GUESTAGENT_ARCH_{ARMV7L, RISCV64, S390X, ...}` to `n` (for Homebrew)


`armv7l`, `riscv64`, and `s390x` (being added via #3319) are very rarely used and should not waste the disk space by default:
```console
$ ls -lh ~/.local/share/lima/lima-guestagent.Linux-*
-rw-r--r--  1 suda  staff    53M  3  9 05:04 /Users/suda/.local/share/lima/lima-guestagent.Linux-aarch64
-rw-r--r--  1 suda  staff    52M  3  9 05:04 /Users/suda/.local/share/lima/lima-guestagent.Linux-armv7l
-rw-r--r--  1 suda  staff    53M  3  9 05:04 /Users/suda/.local/share/lima/lima-guestagent.Linux-riscv64
-rw-r--r--  1 suda  staff    58M  3  9 05:04 /Users/suda/.local/share/lima/lima-guestagent.Linux-s390x
-rw-r--r--  1 suda  staff    55M  3  9 05:04 /Users/suda/.local/share/lima/lima-guestagent.Linux-x86_64
```

My suggestion is:

 Dist  | Config
-------|-------
Source distribution | Just compile `aarch64` and `x86_64` by default. Probably, there should be a script like `./configure --guestagent-arch=all` to allow keeping compiling everything.
Homebrew | Split the `lima` formula to `lima` and `lima-extra-guestagents`. Needs to be negotiated with Homebrew maintainers.
Binary distribution in https://github.com/lima-vm/lima/releases | Should follow the same convention as Homebrew?


The same should apply to other potential guestagents in future too (`ppc64le`, `FreeBSD-x86_64`, etc.)


Hints from issue comments:
@afbjorklund: > Probably, there should be a script like ./configure --guestagent-arch=all to allow keeping compiling everything.

Normally (with konfig) this is handled by providing different config files.

You can even keep the defaults as they are, and only change `config.mk`

config.mk
```
CONFIG_GUESTAGENT_ARCH_X8664=y
CONFIG_GUESTAGENT_ARCH_AARCH64=y
CONFIG_GUESTAGENT_ARCH_ARMV7L=n
CONFIG_GUESTAGENT_ARCH_RISCV64=n
CONFIG_GUESTAGENT_ARCH_S390X=n
```

----

defconfig (`kconfig-conf --savedefconfig defconfig Kconfig`)
```
# CONFIG_GUESTAGENT_ARCH_ARMV7L is not set
# CONFIG_GUESTAGENT_ARCH_RISCV64 is not set
# CONFIG_GUESTAGENT_ARCH_S390X is not set
```

alldefconfig (`kconfig-conf --alldefconfig Kconfig`)
```
CONFIG_GUESTAGENT_ARCH_X8664=y
CONFIG_GUESTAGENT_ARCH_AARCH64=y
CONFIG_GUESTAGENT_ARCH_ARMV7L=y
CONFIG_GUESTAGENT_ARCH_RISCV64=y
CONFIG_GUESTAGENT_ARCH_S390X=y
```
@jandubois: I'm confused; can't we already build whatever we want with a combination of these:

https://github.com/lima-vm/lima/blob/4a88b71430e37562cad05722a1c5697ae144fab4/Makefile#L97-L101

Personally I always thought that the `kconfig` mechanism is kind of weird.@afbjorklund: > Personally I always thought that the `kconfig` mechanism is kind of weird.

I think at this point, the makefile targets could probably replace the Kconfig?

It's a bit doubled up, and having dedicated targets seemed to be preferred.

I mostly used it (as an alternative to autoconf), because **I** was familar with it...@AkihiroSuda: > I think at this point, the makefile targets could probably replace the Kconfig?

👍 @AkihiroSuda: @lima-vm/maintainers 

Are we safe to do this in v1.1, or do we want to revisit this in v1.2?


> 
> Dist | Config
> -- | --
> Source distribution | Just compile aarch64 and x86_64 by default. Probably, there should be a script like ./configure --guestagent-arch=all to allow keeping compiling everything.
> Homebrew | Split the lima formula to lima and lima-extra-guestagents. Needs to be negotiated with Homebrew maintainers.
> Binary distribution in https://github.com/lima-vm/lima/releases | Should follow the same convention as Homebrew?
> 
> @jandubois: I don't like the idea of an additional configure script. I think we should just add an `all-guestagents` target, and redefine `guestagents` to mean `aarch64` and `x86_64` unconditionally.

For Homebrew it makes sense to me to just include `aarch64` and `x86_64` guestagents. I don't think the additional guestagents need to be packaged, but it is up to the Homebrew maintainers.

For the binary distribution I don't mind including all guestagents. I don't know who is using the binary releases, but the download numbers are small compared to Homebrew installs:

```console
$ gh api repos/lima-vm/lima/releases --jq '
  [ ["Tag", "Asset", "Downloads"] ] +
  (map( .tag_name as $tag | .assets | map([ $tag, .name, .download_count ]) ) | add)
  | .[] | @tsv' | grep -vE '(alpha|beta|go-mod|SHA)' | column -s $'\t' -t
Tag      Asset                             Downloads
v1.0.7   lima-1.0.7-Darwin-arm64.tar.gz    1052
v1.0.7   lima-1.0.7-Darwin-x86_64.tar.gz   1547
v1.0.7   lima-1.0.7-Linux-aarch64.tar.gz   35
v1.0.7   lima-1.0.7-Linux-x86_64.tar.gz    5172
v1.0.6   lima-1.0.6-Darwin-arm64.tar.gz    1287
v1.0.6   lima-1.0.6-Darwin-x86_64.tar.gz   2225
v1.0.6   lima-1.0.6-Linux-aarch64.tar.gz   84
v1.0.6   lima-1.0.6-Linux-x86_64.tar.gz    6059
…

❯ brew info lima
==> lima: stable 1.0.7 (bottled), HEAD
…
==> Analytics
install: 14,704 (30 days), 51,154 (90 days), 192,827 (365 days)
install-on-request: 7,899 (30 days), 28,141 (90 days), 122,289 (365 days)
```

I just checked `brew info lima` on Intel, and the Analytics are the same, so these look like combined platform numbers.

Solution patch:
diff --git a/Kconfig b/Kconfig
index 3c8853fe..31850bce 100644
--- a/Kconfig
+++ b/Kconfig
@@ -31,6 +31,12 @@ config GUESTAGENT_ARCH_RISCV64
 		Build lima-guestagent for "riscv64" Arch
 	default y
 
+config GUESTAGENT_ARCH_S390X
+	bool "guestagent Arch: s390x"
+	help
+		Build lima-guestagent for "s390x" Arch
+	default y
+
 config GUESTAGENT_COMPRESS
 	bool "guestagent compress"
 	help
diff --git a/Makefile b/Makefile
index ef472651..c11b04d3 100644
--- a/Makefile
+++ b/Makefile
@@ -272,7 +272,7 @@ LINUX_GUESTAGENT_PATH_COMMON = _output/share/lima/lima-guestagent.Linux-
 # How to add architecture specific guestagent:
 # 1. Add the architecture to GUESTAGENT_ARCHS
 # 2. Add ENVS_$(*_GUESTAGENT_PATH_COMMON)<arch> to set GOOS, GOARCH, and other necessary environment variables
-LINUX_GUESTAGENT_ARCHS = aarch64 armv7l riscv64 x86_64
+LINUX_GUESTAGENT_ARCHS = aarch64 armv7l riscv64 s390x x86_64
 
 ifeq ($(CONFIG_GUESTAGENT_OS_LINUX),y)
 ALL_GUESTAGENTS_NOT_COMPRESSED += $(addprefix $(LINUX_GUESTAGENT_PATH_COMMON),$(LINUX_GUESTAGENT_ARCHS))
@@ -321,6 +321,7 @@ additional-guestagents: $(ADDITIONAL_GUESTAGENTS)
 ENVS_$(LINUX_GUESTAGENT_PATH_COMMON)aarch64 = CGO_ENABLED=0 GOOS=linux GOARCH=arm64
 ENVS_$(LINUX_GUESTAGENT_PATH_COMMON)armv7l = CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7
 ENVS_$(LINUX_GUESTAGENT_PATH_COMMON)riscv64 = CGO_ENABLED=0 GOOS=linux GOARCH=riscv64
+ENVS_$(LINUX_GUESTAGENT_PATH_COMMON)s390x = CGO_ENABLED=0 GOOS=linux GOARCH=s390x
 ENVS_$(LINUX_GUESTAGENT_PATH_COMMON)x86_64 = CGO_ENABLED=0 GOOS=linux GOARCH=amd64
 $(ALL_GUESTAGENTS_NOT_COMPRESSED): $(call dependencies_for_cmd,lima-guestagent) $$(call force_build_with_gunzip,$$@) | _output/share/lima
 	$(ENVS_$@) $(GO_BUILD) -o $@ ./cmd/lima-guestagent
diff --git a/cmd/limactl/disk.go b/cmd/limactl/disk.go
index d8fca076..a8299a79 100644
--- a/cmd/limactl/disk.go
+++ b/cmd/limactl/disk.go
@@ -12,6 +12,7 @@ import (
 	"text/tabwriter"
 
 	"github.com/docker/go-units"
+	"github.com/lima-vm/lima/pkg/nativeimgutil"
 	"github.com/lima-vm/lima/pkg/qemu"
 	"github.com/lima-vm/lima/pkg/store"
 	"github.com/sirupsen/logrus"
@@ -104,7 +105,13 @@ func diskCreateAction(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	if err := qemu.CreateDataDisk(diskDir, format, int(diskSize)); err != nil {
+	// qemu may not be available, use it only if needed.
+	if format == "raw" {
+		err = nativeimgutil.CreateRawDataDisk(diskDir, int(diskSize))
+	} else {
+		err = qemu.CreateDataDisk(diskDir, format, int(diskSize))
+	}
+	if err != nil {
 		rerr := os.RemoveAll(diskDir)
 		if rerr != nil {
 			err = errors.Join(err, fmt.Errorf("failed to remove a directory %q: %w", diskDir, rerr))
@@ -393,9 +400,17 @@ func diskResizeAction(cmd *cobra.Command, args []string) error {
 			}
 		}
 	}
-	if err := qemu.ResizeDataDisk(disk.Dir, disk.Format, int(diskSize)); err != nil {
+
+	// qemu may not be available, use it only if needed.
+	if disk.Format == "raw" {
+		err = nativeimgutil.ResizeRawDataDisk(disk.Dir, int(diskSize))
+	} else {
+		err = qemu.ResizeDataDisk(disk.Dir, disk.Format, int(diskSize))
+	}
+	if err != nil {
 		return fmt.Errorf("failed to resize disk %q: %w", diskName, err)
 	}
+
 	logrus.Infof("Resized disk %q (%q)", diskName, disk.Dir)
 	return nil
 }
diff --git a/cmd/limactl/editflags/editflags.go b/cmd/limactl/editflags/editflags.go
index e5b9a584..da0f266c 100644
--- a/cmd/limactl/editflags/editflags.go
+++ b/cmd/limactl/editflags/editflags.go
@@ -73,9 +73,9 @@ func RegisterCreate(cmd *cobra.Command, commentPrefix string) {
 	registerEdit(cmd, commentPrefix)
 	flags := cmd.Flags()
 
-	flags.String("arch", "", commentPrefix+"machine architecture (x86_64, aarch64, riscv64)") // colima-compatible
+	flags.String("arch", "", commentPrefix+"machine architecture (x86_64, aarch64, riscv64, armv7l, s390x)") // colima-compatible
 	_ = cmd.RegisterFlagCompletionFunc("arch", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
-		return []string{"x86_64", "aarch64", "riscv64"}, cobra.ShellCompDirectiveNoFileComp
+		return []string{"x86_64", "aarch64", "riscv64", "armv7l", "s390x"}, cobra.ShellCompDirectiveNoFileComp
 	})
 
 	flags.String("containerd", "", commentPrefix+"containerd mode (user, system, user+system, none)")
diff --git a/config.mk b/config.mk
index 7b3eb49e..6b7ff313 100644
--- a/config.mk
+++ b/config.mk
@@ -3,4 +3,5 @@ CONFIG_GUESTAGENT_ARCH_X8664=y
 CONFIG_GUESTAGENT_ARCH_AARCH64=y
 CONFIG_GUESTAGENT_ARCH_ARMV7L=y
 CONFIG_GUESTAGENT_ARCH_RISCV64=y
+CONFIG_GUESTAGENT_ARCH_S390X=y
 CONFIG_GUESTAGENT_COMPRESS=n
diff --git a/pkg/guestagent/guestagent_linux.go b/pkg/guestagent/guestagent_linux.go
index 91d1c4d5..f64a672e 100644
--- a/pkg/guestagent/guestagent_linux.go
+++ b/pkg/guestagent/guestagent_linux.go
@@ -20,7 +20,6 @@ import (
 	"github.com/lima-vm/lima/pkg/guestagent/procnettcp"
 	"github.com/lima-vm/lima/pkg/guestagent/timesync"
 	"github.com/sirupsen/logrus"
-	"golang.org/x/sys/cpu"
 	"google.golang.org/protobuf/types/known/timestamppb"
 )
 
@@ -220,9 +219,6 @@ func (a *agent) Events(ctx context.Context, ch chan *api.Event) {
 }
 
 func (a *agent) LocalPorts(_ context.Context) ([]*api.IPPort, error) {
-	if cpu.IsBigEndian {
-		return nil, errors.New("big endian architecture is unsupported, because I don't know how /proc/net/tcp looks like on big endian hosts")
-	}
 	var res []*api.IPPort
 	tcpParsed, err := procnettcp.ParseFiles()
 	if err != nil {
diff --git a/pkg/guestagent/procnettcp/procnettcp.go b/pkg/guestagent/procnettcp/procnettcp.go
index a1145705..2865b7be 100644
--- a/pkg/guestagent/procnettcp/procnettcp.go
+++ b/pkg/guestagent/procnettcp/procnettcp.go
@@ -12,6 +12,8 @@ import (
 	"net"
 	"strconv"
 	"strings"
+
+	"golang.org/x/sys/cpu"
 )
 
 type Kind = string
@@ -40,6 +42,10 @@ type Entry struct {
 }
 
 func Parse(r io.Reader, kind Kind) ([]Entry, error) {
+	return ParseWithEndian(r, kind, cpu.IsBigEndian)
+}
+
+func ParseWithEndian(r io.Reader, kind Kind, isBE bool) ([]Entry, error) {
 	switch kind {
 	case TCP, TCP6, UDP, UDP6:
 	default:
@@ -72,7 +78,7 @@ func Parse(r io.Reader, kind Kind) ([]Entry, error) {
 		default:
 			// localAddress is like "0100007F:053A"
 			localAddress := fields[fieldNames["local_address"]]
-			ip, port, err := ParseAddress(localAddress)
+			ip, port, err := ParseAddressWithEndian(localAddress, isBE)
 			if err != nil {
 				return entries, err
 			}
@@ -99,17 +105,24 @@ func Parse(r io.Reader, kind Kind) ([]Entry, error) {
 	return entries, nil
 }
 
-// ParseAddress parses a string, e.g.,
+// ParseAddress parses a string.
+//
+// Little endian hosts:
 // "0100007F:0050"                         (127.0.0.1:80)
 // "000080FE00000000FF57A6705DC771FE:0050" ([fe80::70a6:57ff:fe71:c75d]:80)
 // "00000000000000000000000000000000:0050" (0.0.0.0:80)
 //
-// See https://serverfault.com/questions/592574/why-does-proc-net-tcp6-represents-1-as-1000
+// Big endian hosts:
+// "7F000001:0050"                         (127.0.0.1:80)
+// "FE8000000000000070A657FFFE71C75D:0050" ([fe80::70a6:57ff:fe71:c75d]:80)
+// "00000000000000000000000000000000:0050" (0.0.0.0:80)
 //
-// ParseAddress is expected to be used for /proc/net/{tcp,tcp6} entries on
-// little endian machines.
-// Not sure how those entries look like on big endian machines.
+// See https://serverfault.com/questions/592574/why-does-proc-net-tcp6-represents-1-as-1000
 func ParseAddress(s string) (net.IP, uint16, error) {
+	return ParseAddressWithEndian(s, cpu.IsBigEndian)
+}
+
+func ParseAddressWithEndian(s string, isBE bool) (net.IP, uint16, error) {
 	split := strings.SplitN(s, ":", 2)
 	if len(split) != 2 {
 		return nil, 0, fmt.Errorf("unparsable address %q", s)
@@ -124,12 +137,18 @@ func ParseAddress(s string) (net.IP, uint16, error) {
 	ipBytes := make([]byte, len(split[0])/2) // 4 bytes (8 chars) or 16 bytes (32 chars)
 	for i := 0; i < len(split[0])/8; i++ {
 		quartet := split[0][8*i : 8*(i+1)]
-		quartetLE, err := hex.DecodeString(quartet) // surprisingly little endian, per 4 bytes
+		quartetB, err := hex.DecodeString(quartet) // surprisingly little endian, per 4 bytes, on little endian hosts
 		if err != nil {
 			return nil, 0, fmt.Errorf("unparsable address %q: unparsable quartet %q: %w", s, quartet, err)
 		}
-		for j := 0; j < len(quartetLE); j++ {
-			ipBytes[4*i+len(quartetLE)-1-j] = quartetLE[j]
+		if isBE {
+			for j := 0; j < len(quartetB); j++ {
+				ipBytes[4*i+j] = quartetB[j]
+			}
+		} else {
+			for j := 0; j < len(quartetB); j++ {
+				ipBytes[4*i+len(quartetB)-1-j] = quartetB[j]
+			}
 		}
 	}
 	ip := net.IP(ipBytes)
diff --git a/pkg/limatmpl/locator.go b/pkg/limatmpl/locator.go
index 5afb7cb6..b270332f 100644
--- a/pkg/limatmpl/locator.go
+++ b/pkg/limatmpl/locator.go
@@ -140,6 +140,7 @@ var archKeywords = map[string]limayaml.Arch{
 	"armhf":   limayaml.ARMV7L,
 	"armv7l":  limayaml.ARMV7L,
 	"riscv64": limayaml.RISCV64,
+	"s390x":   limayaml.S390X,
 	"x86_64":  limayaml.X8664,
 }
 
diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go
index 177f80a7..ef79b800 100644
--- a/pkg/limayaml/defaults.go
+++ b/pkg/limayaml/defaults.go
@@ -65,6 +65,7 @@ func defaultCPUType() CPUType {
 		// Since https://github.com/lima-vm/lima/pull/494, we use qemu64 cpu for better emulation of x86_64.
 		X8664:   "qemu64",
 		RISCV64: "rv64", // FIXME: what is the right choice for riscv64?
+		S390X:   "qemu", // FIXME: what is the right choice for s390x?
 	}
 	for arch := range cpuType {
 		if IsNativeArch(arch) && IsAccelOS() {
@@ -1090,6 +1091,8 @@ func NewArch(arch string) Arch {
 		return arch
 	case "riscv64":
 		return RISCV64
+	case "s390x":
+		return S390X
 	default:
 		logrus.Warnf("Unknown arch: %s", arch)
 		return arch
@@ -1280,7 +1283,8 @@ func IsNativeArch(arch Arch) bool {
 	nativeAARCH64 := arch == AARCH64 && runtime.GOARCH == "arm64"
 	nativeARMV7L := arch == ARMV7L && runtime.GOARCH == "arm" && goarm() == 7
 	nativeRISCV64 := arch == RISCV64 && runtime.GOARCH == "riscv64"
-	return nativeX8664 || nativeAARCH64 || nativeARMV7L || nativeRISCV64
+	nativeS390X := arch == S390X && runtime.GOARCH == "s390x"
+	return nativeX8664 || nativeAARCH64 || nativeARMV7L || nativeRISCV64 || nativeS390X
 }
 
 func unique(s []string) []string {
diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go
index 39a7c557..b954063c 100644
--- a/pkg/limayaml/limayaml.go
+++ b/pkg/limayaml/limayaml.go
@@ -77,6 +77,7 @@ const (
 	AARCH64 Arch = "aarch64"
 	ARMV7L  Arch = "armv7l"
 	RISCV64 Arch = "riscv64"
+	S390X   Arch = "s390x"
 
 	REVSSHFS MountType = "reverse-sshfs"
 	NINEP    MountType = "9p"
@@ -90,7 +91,7 @@ const (
 
 var (
 	OSTypes    = []OS{LINUX}
-	ArchTypes  = []Arch{X8664, AARCH64, ARMV7L, RISCV64}
+	ArchTypes  = []Arch{X8664, AARCH64, ARMV7L, RISCV64, S390X}
 	MountTypes = []MountType{REVSSHFS, NINEP, VIRTIOFS, WSLMount}
 	VMTypes    = []VMType{QEMU, VZ, WSL2}
 )
diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go
index fb93a08b..1dce335c 100644
--- a/pkg/limayaml/validate.go
+++ b/pkg/limayaml/validate.go
@@ -35,9 +35,9 @@ func validateFileObject(f File, fieldName string) error {
 		// f.Location does NOT need to be accessible, so we do NOT check os.Stat(f.Location)
 	}
 	switch f.Arch {
-	case X8664, AARCH64, ARMV7L, RISCV64:
+	case X8664, AARCH64, ARMV7L, RISCV64, S390X:
 	default:
-		return fmt.Errorf("field `arch` must be %q, %q, %q, or %q; got %q", X8664, AARCH64, ARMV7L, RISCV64, f.Arch)
+		return fmt.Errorf("field `arch` must be %q, %q, %q, %q, or %q; got %q", X8664, AARCH64, ARMV7L, RISCV64, S390X, f.Arch)
 	}
 	if f.Digest != "" {
 		if !f.Digest.Algorithm().Available() {
@@ -77,9 +77,9 @@ func Validate(y *LimaYAML, warn bool) error {
 		return fmt.Errorf("field `os` must be %q; got %q", LINUX, *y.OS)
 	}
 	switch *y.Arch {
-	case X8664, AARCH64, ARMV7L, RISCV64:
+	case X8664, AARCH64, ARMV7L, RISCV64, S390X:
 	default:
-		return fmt.Errorf("field `arch` must be %q, %q, %q or %q; got %q", X8664, AARCH64, ARMV7L, RISCV64, *y.Arch)
+		return fmt.Errorf("field `arch` must be %q, %q, %q, %q or %q; got %q", X8664, AARCH64, ARMV7L, RISCV64, S390X, *y.Arch)
 	}
 
 	switch *y.VMType {
@@ -125,7 +125,7 @@ func Validate(y *LimaYAML, warn bool) error {
 
 	for arch := range y.CPUType {
 		switch arch {
-		case AARCH64, X8664, ARMV7L, RISCV64:
+		case AARCH64, X8664, ARMV7L, RISCV64, S390X:
 			// these are the only supported architectures
 		default:
 			return fmt.Errorf("field `cpuType` uses unsupported arch %q", arch)
@@ -578,8 +578,9 @@ func warnExperimental(y *LimaYAML) {
 	if *y.MountType == VIRTIOFS && runtime.GOOS == "linux" {
 		logrus.Warn("`mountType: virtiofs` on Linux is experimental")
 	}
-	if *y.Arch == RISCV64 {
-		logrus.Warn("`arch: riscv64` is experimental")
+	switch *y.Arch {
+	case RISCV64, ARMV7L, S390X:
+		logrus.Warnf("`arch: %s ` is experimental", *y.Arch)
 	}
 	if y.Video.Display != nil && strings.Contains(*y.Video.Display, "vnc") {
 		logrus.Warn("`video.display: vnc` is experimental")
diff --git a/pkg/nativeimgutil/nativeimgutil.go b/pkg/nativeimgutil/nativeimgutil.go
index eca397c9..733f2dc5 100644
--- a/pkg/nativeimgutil/nativeimgutil.go
+++ b/pkg/nativeimgutil/nativeimgutil.go
@@ -5,21 +5,44 @@
 package nativeimgutil
 
 import (
+	"errors"
 	"fmt"
 	"io"
+	"io/fs"
 	"os"
 	"path/filepath"
 
-	"github.com/containerd/continuity/fs"
+	containerdfs "github.com/containerd/continuity/fs"
 	"github.com/docker/go-units"
 	"github.com/lima-vm/go-qcow2reader"
 	"github.com/lima-vm/go-qcow2reader/convert"
 	"github.com/lima-vm/go-qcow2reader/image/qcow2"
 	"github.com/lima-vm/go-qcow2reader/image/raw"
 	"github.com/lima-vm/lima/pkg/progressbar"
+	"github.com/lima-vm/lima/pkg/store/filenames"
 	"github.com/sirupsen/logrus"
 )
 
+// CreateRawDataDisk creates an empty raw data disk.
+func CreateRawDataDisk(dir string, size int) error {
+	dataDisk := filepath.Join(dir, filenames.DataDisk)
+	if _, err := os.Stat(dataDisk); err == nil || !errors.Is(err, fs.ErrNotExist) {
+		return err
+	}
+	f, err := os.Create(dataDisk)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	return f.Truncate(int64(size))
+}
+
+// ResizeRawDataDisk resizes a raw data disk.
+func ResizeRawDataDisk(dir string, size int) error {
+	dataDisk := filepath.Join(dir, filenames.DataDisk)
+	return os.Truncate(dataDisk, int64(size))
+}
+
 // ConvertToRaw converts a source disk into a raw disk.
 // source and dest may be same.
 // ConvertToRaw is a NOP if source == dest, and no resizing is needed.
@@ -109,7 +132,7 @@ func ConvertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b
 func convertRawToRaw(source, dest string, size *int64) error {
 	if source != dest {
 		// continuity attempts clonefile
-		if err := fs.CopyFile(dest, source); err != nil {
+		if err := containerdfs.CopyFile(dest, source); err != nil {
 			return fmt.Errorf("failed to copy %q into %q: %w", source, dest, err)
 		}
 	}
diff --git a/pkg/qemu/qemu.go b/pkg/qemu/qemu.go
index adc75222..cfc1cf9b 100644
--- a/pkg/qemu/qemu.go
+++ b/pkg/qemu/qemu.go
@@ -587,6 +587,9 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er
 	case limayaml.ARMV7L:
 		machine := "virt,accel=" + accel
 		args = appendArgsIfNoConflict(args, "-machine", machine)
+	case limayaml.S390X:
+		machine := "s390-ccw-virtio,accel=" + accel
+		args = appendArgsIfNoConflict(args, "-machine", machine)
 	}
 
 	// SMP
@@ -599,7 +602,8 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er
 		logrus.Warnf("field `firmware.legacyBIOS` is not supported for architecture %q, ignoring", *y.Arch)
 		legacyBIOS = false
 	}
-	if !legacyBIOS {
+	noFirmware := *y.Arch == limayaml.S390X || legacyBIOS
+	if !noFirmware {
 		var firmware string
 		firmwareInBios := runtime.GOOS == "windows"
 		if envVar := os.Getenv("LIMA_QEMU_UEFI_IN_BIOS"); envVar != "" {
@@ -782,7 +786,13 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er
 		}
 		args = append(args, "-netdev", fmt.Sprintf("socket,id=net0,fd={{ fd_connect %q }}", qemuSock))
 	}
-	args = append(args, "-device", "virtio-net-pci,netdev=net0,mac="+limayaml.MACAddress(cfg.InstanceDir))
+	virtioNet := "virtio-net-pci"
+	if *y.Arch == limayaml.S390X {
+		// virtio-net-pci does not work on EL, while it works on Ubuntu
+		// https://github.com/lima-vm/lima/pull/3319/files#r1986388345
+		virtioNet = "virtio-net-ccw"
+	}
+	args = append(args, "-device", virtioNet+",netdev=net0,mac="+limayaml.MACAddress(cfg.InstanceDir))
 
 	for i, nw := range y.Networks {
 		if nw.Lima != "" {
@@ -805,7 +815,7 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er
 					return "", nil, err
 				}
 				args = append(args, "-netdev", fmt.Sprintf("socket,id=net%d,fd={{ fd_connect %q }}", i+1, qemuSock))
-				args = append(args, "-device", fmt.Sprintf("virtio-net-pci,netdev=net%d,mac=%s", i+1, nw.MACAddress))
+				args = append(args, "-device", fmt.Sprintf("%s,netdev=net%d,mac=%s", virtioNet, i+1, nw.MACAddress))
 			} else {
 				if runtime.GOOS != "darwin" {
 					return "", nil, fmt.Errorf("networks.yaml '%s' configuration is only supported on macOS right now", nw.Lima)
@@ -824,7 +834,7 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er
 		} else {
 			return "", nil, fmt.Errorf("invalid network spec %+v", nw)
 		}
-		args = append(args, "-device", fmt.Sprintf("virtio-net-pci,netdev=net%d,mac=%s", i+1, nw.MACAddress))
+		args = append(args, "-device", fmt.Sprintf("%s,netdev=net%d,mac=%s", virtioNet, i+1, nw.MACAddress))
 	}
 
 	// virtio-rng-pci accelerates starting up the OS, according to https://wiki.gentoo.org/wiki/QEMU/Options
@@ -866,7 +876,7 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er
 		args = append(args, "-device", "virtio-keyboard-pci")
 		args = append(args, "-device", "virtio-"+input+"-pci")
 		args = append(args, "-device", "qemu-xhci,id=usb-bus")
-	case limayaml.AARCH64, limayaml.ARMV7L:
+	case limayaml.AARCH64, limayaml.ARMV7L, limayaml.S390X:
 		if features.VersionGEQ7 {
 			args = append(args, "-device", "virtio-gpu")
 			args = append(args, "-device", "virtio-keyboard-pci")
@@ -926,7 +936,11 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er
 	const serialvChardev = "char-serial-virtio"
 	args = append(args, "-chardev", fmt.Sprintf("socket,id=%s,path=%s,server=on,wait=off,logfile=%s", serialvChardev, serialvSock, serialvLog))
 	// max_ports=1 is required for https://github.com/lima-vm/lima/issues/1689 https://github.com/lima-vm/lima/issues/1691
-	args = append(args, "-device", "virtio-serial-pci,id=virtio-serial0,max_ports=1")
+	serialvMaxPorts := 1
+	if *y.Arch == limayaml.S390X {
+		serialvMaxPorts++ // needed to avoid `virtio-serial-bus: Out-of-range port id specified, max. allowed: 0`
+	}
+	args = append(args, "-device", fmt.Sprintf("virtio-serial-pci,id=virtio-serial0,max_ports=%d", serialvMaxPorts))
 	args = append(args, "-device", fmt.Sprintf("virtconsole,chardev=%s,id=console0", serialvChardev))
 
 	// We also want to enable vsock here, but QEMU does not support vsock for macOS hosts
diff --git a/templates/almalinux-8.yaml b/templates/almalinux-8.yaml
index e5f1f270..f42327e8 100644
--- a/templates/almalinux-8.yaml
+++ b/templates/almalinux-8.yaml
@@ -10,12 +10,17 @@ images:
 - location: "https://repo.almalinux.org/almalinux/8.10/cloud/aarch64/images/AlmaLinux-8-GenericCloud-8.10-20240819.aarch64.qcow2"
   arch: "aarch64"
   digest: "sha256:cec6736cbc562d06895e218b6f022621343c553bfa79192ca491381b4636c7b8"
+- location: "https://repo.almalinux.org/almalinux/8.10/cloud/s390x/images/AlmaLinux-8-GenericCloud-8.10-20240819.s390x.qcow2"
+  arch: "s390x"
+  digest: "sha256:7f8866a4247ad57c81f5d2c5a0fa64940691f9df1e858a1510d34a0de008eb16"
 # Fallback to the latest release image.
 # Hint: run `limactl prune` to invalidate the cache
 - location: "https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/AlmaLinux-8-GenericCloud-latest.x86_64.qcow2"
   arch: "x86_64"
 - location: "https://repo.almalinux.org/almalinux/8/cloud/aarch64/images/AlmaLinux-8-GenericCloud-latest.aarch64.qcow2"
   arch: "aarch64"
+- location: "https://repo.almalinux.org/almalinux/8/cloud/s390x/images/AlmaLinux-8-GenericCloud-latest.s390x.qcow2"
+  arch: "s390x"
 mountTypesUnsupported: ["9p"]
 mounts:
 - location: "~"
diff --git a/templates/almalinux-9.yaml b/templates/almalinux-9.yaml
index ef7f4a09..2e727243 100644
--- a/templates/almalinux-9.yaml
+++ b/templates/almalinux-9.yaml
@@ -7,12 +7,17 @@ images:
 - location: "https://repo.almalinux.org/almalinux/9.5/cloud/aarch64/images/AlmaLinux-9-GenericCloud-9.5-20241120.aarch64.qcow2"
   arch: "aarch64"
   digest: "sha256:5ede4affaad0a997a2b642f1628b6268bd8eba775f281346b75be3ed20ec369e"
+- location: "https://repo.almalinux.org/almalinux/9.5/cloud/s390x/images/AlmaLinux-9-GenericCloud-9.5-20241120.s390x.qcow2"
+  arch: "s390x"
+  digest: "sha256:9948ea1c5ee1c6497bde5cec18c71c5f9e50861af4c0f1400c1af504eee2b995"
 # Fallback to the latest release image.
 # Hint: run `limactl prune` to invalidate the cache
 - location: "https://repo.almalinux.org/almalinux/9/cloud/x86_64/images/AlmaLinux-9-GenericCloud-latest.x86_64.qcow2"
   arch: "x86_64"
 - location: "https://repo.almalinux.org/almalinux/9/cloud/aarch64/images/AlmaLinux-9-GenericCloud-latest.aarch64.qcow2"
   arch: "aarch64"
+- location: "https://repo.almalinux.org/almalinux/9/cloud/s390x/images/AlmaLinux-9-GenericCloud-latest.s390x.qcow2"
+  arch: "s390x"
 mountTypesUnsupported: ["9p"]
 mounts:
 - location: "~"
diff --git a/templates/centos-stream-10.yaml b/templates/centos-stream-10.yaml
index 1d985ee4..cbbe7729 100644
--- a/templates/centos-stream-10.yaml
+++ b/templates/centos-stream-10.yaml
@@ -8,12 +8,17 @@ images:
 - location: "https://cloud.centos.org/centos/10-stream/aarch64/images/CentOS-Stream-GenericCloud-10-20250210.0.aarch64.qcow2"
   arch: "aarch64"
   digest: "sha256:996cdea7be4e13b04a71a45e89215e85c2909d5e08d6f1a8faaa66e7e2f93cbb"
+- location: "https://cloud.centos.org/centos/10-stream/s390x/images/CentOS-Stream-GenericCloud-10-20250210.0.s390x.qcow2"
+  arch: "s390x"
+  digest: "sha256:ed39940423de3b9b84bdef9fd155fdb09384d099dca4cb036d283fa27668f6f3"
 # Fallback to the latest release image.
 # Hint: run `limactl prune` to invalidate the cache
 - location: "https://cloud.centos.org/centos/10-stream/x86_64/images/CentOS-Stream-GenericCloud-10-latest.x86_64.qcow2"
   arch: "x86_64"
 - location: "https://cloud.centos.org/centos/10-stream/aarch64/images/CentOS-Stream-GenericCloud-10-latest.aarch64.qcow2"
   arch: "aarch64"
+- location: "https://cloud.centos.org/centos/10-stream/s390x/images/CentOS-Stream-GenericCloud-10-latest.s390x.qcow2"
+  arch: "s390x"
 mountTypesUnsupported: ["9p"]
 mounts:
 - location: "~"
diff --git a/templates/centos-stream-9.yaml b/templates/centos-stream-9.yaml
index 277a3052..ee0a977d 100644
--- a/templates/centos-stream-9.yaml
+++ b/templates/centos-stream-9.yaml
@@ -8,12 +8,17 @@ images:
 - location: "https://cloud.centos.org/centos/9-stream/aarch64/images/CentOS-Stream-GenericCloud-9-20250210.0.aarch64.qcow2"
   arch: "aarch64"
   digest: "sha256:507f3e49c8a62eed63e73887b7cdb97a4a1382f6a20ded1eb5eed69c18d9197f"
+- location: "https://cloud.centos.org/centos/9-stream/s390x/images/CentOS-Stream-GenericCloud-9-20250210.0.s390x.qcow2"
+  arch: "s390x"
+  digest: "sha256:f347daf1008a3efefcb9da4ee7cf06e38226d2f81f606770b07dbebe5b172552"
 # Fallback to the latest release image.
 # Hint: run `limactl prune` to invalidate the cache
 - location: "https://cloud.centos.org/centos/9-stream/x86_64/images/CentOS-Stream-GenericCloud-9-latest.x86_64.qcow2"
   arch: "x86_64"
 - location: "https://cloud.centos.org/centos/9-stream/aarch64/images/CentOS-Stream-GenericCloud-9-latest.aarch64.qcow2"
   arch: "aarch64"
+- location: "https://cloud.centos.org/centos/9-stream/s390x/images/CentOS-Stream-GenericCloud-9-latest.s390x.qcow2"
+  arch: "s390x"
 mountTypesUnsupported: ["9p"]
 mounts:
 - location: "~"
diff --git a/templates/default.yaml b/templates/default.yaml
index 181b66e7..c3f29c51 100644
--- a/templates/default.yaml
+++ b/templates/default.yaml
@@ -32,6 +32,9 @@ images:
 - location: "https://cloud-images.ubuntu.com/releases/24.10/release-20250129/ubuntu-24.10-server-cloudimg-armhf.img"
   arch: "armv7l"
   digest: "sha256:6a1ca78f06227ba060d11a88009cc9b65bdd42e17b84d444d204cb5c80f81552"
+- location: "https://cloud-images.ubuntu.com/releases/24.10/release-20250129/ubuntu-24.10-server-cloudimg-s390x.img"
+  arch: "s390x"
+  digest: "sha256:940d835ebfb4de02ba93d46bce75ab5602f36f8a66c0865df579205aa02d28db"
 # Fallback to the latest release image.
 # Hint: run `limactl prune` to invalidate the cache
 - location: "https://cloud-images.ubuntu.com/releases/24.10/release/ubuntu-24.10-server-cloudimg-amd64.img"
@@ -42,6 +45,8 @@ images:
   arch: "riscv64"
 - location: "https://cloud-images.ubuntu.com/releases/24.10/release/ubuntu-24.10-server-cloudimg-armhf.img"
   arch: "armv7l"
+- location: "https://cloud-images.ubuntu.com/releases/24.10/release/ubuntu-24.10-server-cloudimg-s390x.img"
+  arch: "s390x"
 # CPUs
 # 🟢 Builtin default: min(4, host CPU cores)
 cpus: null
diff --git a/templates/ubuntu-20.04.yaml b/templates/ubuntu-20.04.yaml
index cd0041f2..4c554992 100644
--- a/templates/ubuntu-20.04.yaml
+++ b/templates/ubuntu-20.04.yaml
@@ -10,6 +10,9 @@ images:
 - location: "https://cloud-images.ubuntu.com/releases/focal/release-20250127/ubuntu-20.04-server-cloudimg-armhf.img"
   arch: "armv7l"
   digest: "sha256:027579b19db39e989a95129440fbbc495d9d7426fedde7548851dac1693f8312"
+- location: "https://cloud-images.ubuntu.com/releases/focal/release-20250127/ubuntu-20.04-server-cloudimg-s390x.img"
+  arch: "s390x"
+  digest: "sha256:20306930392db13ef3bf169d236b4d67cd1c84e606df05bc4539b0f55d00ad74"
 # Fallback to the latest release image.
 # Hint: run `limactl prune` to invalidate the cache
 - location: "https://cloud-images.ubuntu.com/releases/focal/release/ubuntu-20.04-server-cloudimg-amd64.img"
@@ -18,6 +21,8 @@ images:
   arch: "aarch64"
 - location: "https://cloud-images.ubuntu.com/releases/focal/release/ubuntu-20.04-server-cloudimg-armhf.img"
   arch: "armv7l"
+- location: "https://cloud-images.ubuntu.com/releases/focal/release/ubuntu-20.04-server-cloudimg-s390x.img"
+  arch: "s390x"
 
 mounts:
 - location: "~"
diff --git a/templates/ubuntu-22.04.yaml b/templates/ubuntu-22.04.yaml
index c25956ed..012290aa 100644
--- a/templates/ubuntu-22.04.yaml
+++ b/templates/ubuntu-22.04.yaml
@@ -13,6 +13,9 @@ images:
 - location: "https://cloud-images.ubuntu.com/releases/22.04/release-20250128/ubuntu-22.04-server-cloudimg-armhf.img"
   arch: "armv7l"
   digest: "sha256:2a3fce7f82426204b1aa44312c3cfb2a107de2204eea6a0bf1221c2541ee7f3f"
+- location: "https://cloud-images.ubuntu.com/releases/22.04/release-20250128/ubuntu-22.04-server-cloudimg-s390x.img"
+  arch: "s390x"
+  digest: "sha256:fa287d4ab8ff236bfe0dc1e6253159ecfa0734b6d682d0006977a46a53984d67"
 # Fallback to the latest release image.
 # Hint: run `limactl prune` to invalidate the cache
 - location: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.img"
@@ -23,6 +26,8 @@ images:
   arch: "riscv64"
 - location: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-armhf.img"
   arch: "armv7l"
+- location: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-s390x.img"
+  arch: "s390x"
 
 mounts:
 - location: "~"
diff --git a/templates/ubuntu-24.04.yaml b/templates/ubuntu-24.04.yaml
index ffe256c7..3dc64453 100644
--- a/templates/ubuntu-24.04.yaml
+++ b/templates/ubuntu-24.04.yaml
@@ -13,6 +13,9 @@ images:
 - location: "https://cloud-images.ubuntu.com/releases/24.04/release-20250115/ubuntu-24.04-server-cloudimg-armhf.img"
   arch: "armv7l"
   digest: "sha256:c9ad061c976443d8d4831da1a28aa9ac192d392d392ac287e5ae873d9b00cbcd"
+- location: "https://cloud-images.ubuntu.com/releases/24.04/release-20250115/ubuntu-24.04-server-cloudimg-s390x.img"
+  arch: "s390x"
+  digest: "sha256:367eef35e239ebd123bd00a1fb66cd048604b70bca362eccad1f95b0021d7458"
 # Fallback to the latest release image.
 # Hint: run `limactl prune` to invalidate the cache
 - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img"
@@ -23,6 +26,8 @@ images:
   arch: "riscv64"
 - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-armhf.img"
   arch: "armv7l"
+- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-s390x.img"
+  arch: "s390x"
 
 mounts:
 - location: "~"
diff --git a/templates/ubuntu-24.10.yaml b/templates/ubuntu-24.10.yaml
index 9be2249f..fac0ce00 100644
--- a/templates/ubuntu-24.10.yaml
+++ b/templates/ubuntu-24.10.yaml
@@ -13,6 +13,9 @@ images:
 - location: "https://cloud-images.ubuntu.com/releases/24.10/release-20250129/ubuntu-24.10-server-cloudimg-armhf.img"
   arch: "armv7l"
   digest: "sha256:6a1ca78f06227ba060d11a88009cc9b65bdd42e17b84d444d204cb5c80f81552"
+- location: "https://cloud-images.ubuntu.com/releases/24.10/release-20250129/ubuntu-24.10-server-cloudimg-s390x.img"
+  arch: "s390x"
+  digest: "sha256:940d835ebfb4de02ba93d46bce75ab5602f36f8a66c0865df579205aa02d28db"
 # Fallback to the latest release image.
 # Hint: run `limactl prune` to invalidate the cache
 - location: "https://cloud-images.ubuntu.com/releases/24.10/release/ubuntu-24.10-server-cloudimg-amd64.img"
@@ -23,6 +26,8 @@ images:
   arch: "riscv64"
 - location: "https://cloud-images.ubuntu.com/releases/24.10/release/ubuntu-24.10-server-cloudimg-armhf.img"
   arch: "armv7l"
+- location: "https://cloud-images.ubuntu.com/releases/24.10/release/ubuntu-24.10-server-cloudimg-s390x.img"
+  arch: "s390x"
 mounts:
 - location: "~"
 - location: "/tmp/lima"
diff --git a/website/content/en/docs/releases/experimental.md b/website/content/en/docs/releases/experimental.md
index d443fce8..da007dc5 100644
--- a/website/content/en/docs/releases/experimental.md
+++ b/website/content/en/docs/releases/experimental.md
@@ -8,10 +8,9 @@ The following features are experimental and subject to change:
 
 - `mountType: virtiofs` on Linux
 - `vmType: wsl2` and relevant configurations (`mountType: wsl2`)
-- `arch: riscv64`
+- `arch: riscv64`, `arch: armv7l`, and `arch: s390x`
 - `video.display: vnc` and relevant configuration (`video.vnc.display`)
 - `audio.device`
-- `arch: armv7l`
 - `mountInotify: true`
 - `base:` and variations like `base[]:` and `base[].url`
 - `provision[].file:` and alternative `provision[].file.url:`  



Test patch:
diff --git a/pkg/guestagent/procnettcp/procnettcp_test.go b/pkg/guestagent/procnettcp/procnettcp_test.go
index f61467ee..b92a9c97 100644
--- a/pkg/guestagent/procnettcp/procnettcp_test.go
+++ b/pkg/guestagent/procnettcp/procnettcp_test.go
@@ -12,6 +12,7 @@ import (
 )
 
 func TestParseTCP(t *testing.T) {
+	const isBE = false
 	procNetTCP := `  sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode                                                     
    0: 0100007F:8AEF 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 28152 1 0000000000000000 100 0 0 10 0                     
    1: 0103000A:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 31474 1 0000000000000000 100 0 0 10 5                     
@@ -20,7 +21,7 @@ func TestParseTCP(t *testing.T) {
    4: 0100007F:053A 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 31430 1 0000000000000000 100 0 0 10 0                     
    5: 0B3CA8C0:0016 690AA8C0:F705 01 00000000:00000000 02:00028D8B 00000000     0        0 32989 4 0000000000000000 20 4 31 10 19
 `
-	entries, err := Parse(strings.NewReader(procNetTCP), TCP)
+	entries, err := ParseWithEndian(strings.NewReader(procNetTCP), TCP, isBE)
 	assert.NilError(t, err)
 	t.Log(entries)
 
@@ -34,9 +35,10 @@ func TestParseTCP(t *testing.T) {
 }
 
 func TestParseTCP6(t *testing.T) {
+	const isBE = false
 	procNetTCP := `  sl  local_address                         remote_address                        st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
 	   0: 000080FE00000000FF57A6705DC771FE:0050 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 850222 1 0000000000000000 100 0 0 10 0`
-	entries, err := Parse(strings.NewReader(procNetTCP), TCP6)
+	entries, err := ParseWithEndian(strings.NewReader(procNetTCP), TCP6, isBE)
 	assert.NilError(t, err)
 	t.Log(entries)
 
@@ -46,12 +48,13 @@ func TestParseTCP6(t *testing.T) {
 }
 
 func TestParseTCP6Zero(t *testing.T) {
+	const isBE = false
 	procNetTCP := `  sl  local_address                         remote_address                        st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
    0: 00000000000000000000000000000000:0016 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 33825 1 0000000000000000 100 0 0 10 0
    1: 00000000000000000000000000000000:006F 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 26772 1 0000000000000000 100 0 0 10 0
    2: 00000000000000000000000000000000:0050 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 1210901 1 0000000000000000 100 0 0 10 0
 `
-	entries, err := Parse(strings.NewReader(procNetTCP), TCP6)
+	entries, err := ParseWithEndian(strings.NewReader(procNetTCP), TCP6, isBE)
 	assert.NilError(t, err)
 	t.Log(entries)
 
@@ -61,13 +64,14 @@ func TestParseTCP6Zero(t *testing.T) {
 }
 
 func TestParseUDP(t *testing.T) {
+	const isBE = false
 	procNetTCP := `   sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode ref pointer drops            
   716: 3600007F:0035 00000000:0000 07 00000000:00000000 00:00000000 00000000   991        0 2964 2 0000000000000000 0          
   716: 3500007F:0035 00000000:0000 07 00000000:00000000 00:00000000 00000000   991        0 2962 2 0000000000000000 0          
   731: 0369A8C0:0044 00000000:0000 07 00000000:00000000 00:00000000 00000000   998        0 29132 2 0000000000000000 0         
   731: 0F05A8C0:0044 00000000:0000 07 00000000:00000000 00:00000000 00000000   998        0 4049 2 0000000000000000 0          
  1768: 00000000:1451 00000000:0000 07 00000000:00000000 00:00000000 00000000   502        0 28364 2 0000000000000000 0  `
-	entries, err := Parse(strings.NewReader(procNetTCP), UDP)
+	entries, err := ParseWithEndian(strings.NewReader(procNetTCP), UDP, isBE)
 	assert.NilError(t, err)
 	t.Log(entries)
 
@@ -79,6 +83,7 @@ func TestParseUDP(t *testing.T) {
 func TestParseAddress(t *testing.T) {
 	tests := []struct {
 		input             string
+		bigEndian         bool
 		expectedIP        net.IP
 		expectedPort      uint16
 		expectedErrSubstr string
@@ -88,13 +93,31 @@ func TestParseAddress(t *testing.T) {
 			expectedIP:   net.IPv4(127, 0, 0, 1),
 			expectedPort: 80,
 		},
+		{
+			input:        "7F000001:0050",
+			bigEndian:    true,
+			expectedIP:   net.IPv4(127, 0, 0, 1),
+			expectedPort: 80,
+		},
 		{
 			input:        "000080FE00000000FF57A6705DC771FE:0050",
 			expectedIP:   net.ParseIP("fe80::70a6:57ff:fe71:c75d"),
 			expectedPort: 80,
 		},
+		{
+			input:        "FE8000000000000070A657FFFE71C75D:0050",
+			bigEndian:    true,
+			expectedIP:   net.ParseIP("fe80::70a6:57ff:fe71:c75d"),
+			expectedPort: 80,
+		},
+		{
+			input:        "00000000000000000000000000000000:0050",
+			expectedIP:   net.IPv6zero,
+			expectedPort: 80,
+		},
 		{
 			input:        "00000000000000000000000000000000:0050",
+			bigEndian:    true,
 			expectedIP:   net.IPv6zero,
 			expectedPort: 80,
 		},
@@ -126,7 +149,7 @@ func TestParseAddress(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.input, func(t *testing.T) {
-			ip, port, err := ParseAddress(test.input)
+			ip, port, err := ParseAddressWithEndian(test.input, test.bigEndian)
 			if test.expectedErrSubstr != "" {
 				assert.ErrorContains(t, err, test.expectedErrSubstr)
 			} else {
diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go
index 71f7e981..b666880f 100644
--- a/pkg/limayaml/defaults_test.go
+++ b/pkg/limayaml/defaults_test.go
@@ -50,6 +50,8 @@ func TestFillDefault(t *testing.T) {
 		arch = ARMV7L
 	case "riscv64":
 		arch = RISCV64
+	case "s390x":
+		arch = S390X
 	default:
 		t.Skipf("unknown GOARCH: %s", runtime.GOARCH)
 	}
@@ -335,6 +337,7 @@ func TestFillDefault(t *testing.T) {
 			ARMV7L:  "armhf",
 			X8664:   "amd64",
 			RISCV64: "riscv64",
+			S390X:   "s390x",
 		},
 		CPUs:   ptr.Of(7),
 		Memory: ptr.Of("5GiB"),
@@ -547,6 +550,7 @@ func TestFillDefault(t *testing.T) {
 			ARMV7L:  "armv8",
 			X8664:   "pentium",
 			RISCV64: "sifive-u54",
+			S390X:   "z14",
 		},
 		CPUs:   ptr.Of(12),
 		Memory: ptr.Of("7GiB"),