Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd/link: allow specifying the macOS SDK in internal linking mode #58722

Open
nguyen-phillip opened this issue Feb 24, 2023 · 10 comments
Open

cmd/link: allow specifying the macOS SDK in internal linking mode #58722

nguyen-phillip opened this issue Feb 24, 2023 · 10 comments
Labels
compiler/runtime Issues related to the Go compiler and/or runtime. NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Milestone

Comments

@nguyen-phillip
Copy link

It appears that the linker hardcodes the LC_BUILD_VERSION1 to target a min SDK of 10.13.0 for amd64 and 11.0 for arm64.

Is there a way to set this during "go build"?

Footnotes

  1. https://github.com/golang/go/blob/203e59ad41bd288e1d92b6f617c2f55e70d3c8e3/src/cmd/link/internal/ld/macho.go#L486

@randall77
Copy link
Contributor

You can use external linking and specify it as a flag to the linker.

@nguyen-phillip
Copy link
Author

Thank you, this got me further and you are correct that

go build -ldflags="-linkmode=external -extldflags=-mmacosx-version-min=12.0" example.go

works if I'm compiling from macOS. However I didn't do a great job of describing my use case. I would like to be able to cross-compile a Go binary for macOS (from a Linux host) while also being able to set the target minimum macOS SDK. In this case I need to be able to use the internal linker. Would it be possible to reopen this bug as a feature request to add a flag to the internal linker so that the target minimum macOS version could be specified during cross-compilation?

The context for this is that we were building a Go binary that read /System/Library/CoreServices/SystemVersion.plist on macOS. There is code in the kernel that changes how the contents of this file are read depending on what the target minimum macOS SDK is of the running binary. Our current build process makes compiling this code natively somewhat difficult, whereas cross-compiling is relatively easy. But this leads to having an x86_64 slice that reads the incorrect OS version from this file.

@ianlancetaylor
Copy link
Contributor

Reopening.

@ianlancetaylor ianlancetaylor reopened this Mar 1, 2023
@ianlancetaylor ianlancetaylor changed the title Allow specifying the macOS SDK when linking cmd/link: allow specifying the macOS SDK in internal linking mode Mar 1, 2023
@ianlancetaylor ianlancetaylor added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Mar 1, 2023
@gopherbot gopherbot added the compiler/runtime Issues related to the Go compiler and/or runtime. label Mar 1, 2023
@ianlancetaylor ianlancetaylor added this to the Backlog milestone Mar 1, 2023
@ianlancetaylor
Copy link
Contributor

CC @cherrymui @thanm

@thanm
Copy link
Contributor

thanm commented Mar 1, 2023

This seems doable in principle, although I worry a bit about adding this sort of flag/knob, since each flag increases the number of scenarios/permutations that we have to test (and if we have N flags, then potentially we have to test all possible combinations).

@cherrymui
Copy link
Member

Sorry, I'm not sure I really understand the purpose of increasing the SDK version, or what exactly does that kernel code do. Could you add more information? Are you trying to enable new feature? Or is that some macOS feature is not compatible with older version of SDK? Thanks.

@randall77
Copy link
Contributor

You can do this as a postprocess on your binary. This little program will change the version from the default of 10.13 to 10.14:

package main

import (
	"debug/macho"
	"encoding/binary"
	"os"
	"unsafe"
)

func main() {
	// Open file.
	f, err := os.OpenFile(os.Args[1], os.O_RDWR, 0)
	if err != nil {
		panic(err)
	}

	// Read and check magic number.
	var ident [4]byte
	if _, err := f.ReadAt(ident[:], 0); err != nil {
		panic(err)
	}
	magic := binary.LittleEndian.Uint32(ident[:])
	if magic != macho.Magic32 && magic != macho.Magic64 {
		panic("not a macho file")
	}

	// Read file header.
	var header macho.FileHeader
	if err := binary.Read(f, binary.LittleEndian, &header); err != nil {
		panic(err)
	}

	// Read load commands.
	offset := int64(unsafe.Sizeof(header))
	if magic == macho.Magic64 {
		offset += 4
	}
	cmds := make([]byte, header.Cmdsz)
	_, err = f.ReadAt(cmds, offset)
	if err != nil {
		panic(err)
	}

	// Iterate through load commands.
	for len(cmds) > 0 {
		cmd := binary.LittleEndian.Uint32(cmds[0:4])
		cmdSize := binary.LittleEndian.Uint32(cmds[4:8])
		if cmd == LC_BUILD_VERSION {
			version := []byte{14} // aka 10.14
			_, err = f.WriteAt(version, offset+13)
			if err != nil {
				panic(err)
			}
			_, err = f.WriteAt(version, offset+17)
			if err != nil {
				panic(err)
			}
			break
		}
		cmds = cmds[cmdSize:]
		offset += int64(cmdSize)
	}
}

const LC_BUILD_VERSION = 50

@nguyen-phillip
Copy link
Author

nguyen-phillip commented Mar 1, 2023

Sorry, I'm not sure I really understand the purpose of increasing the SDK version, or what exactly does that kernel code do. Could you add more information? Are you trying to enable new feature? Or is that some macOS feature is not compatible with older version of SDK?

The kernel code changes the contents of /System/Library/CoreServices/SystemVersion.plist based on the sdk specified in the LC_BUILD_VERSION of the running binary. When sdk predates 11.0, this file changes to display a ProductVersion of 10.16. When sdk is 11.0 or greater, then the true contents of the file are returned. See also https://eclecticlight.co/2020/08/13/macos-version-numbering-isnt-so-simple/. Repro code looks like

// example.go
package main

import (
	"fmt"
	"log"
	"os"
)

func main() {
	b, err := os.ReadFile("/System/Library/CoreServices/SystemVersion.plist")
	if err != nil {
		log.Fatalf("%v", err)
	}
	fmt.Println(string(b))
}

if you compile this with GOOS=darwin GOARCH=amd64 go build example.go, then since

% vtool -show-build example
example:
Load command 5
      cmd LC_BUILD_VERSION
  cmdsize 24
 platform MACOS
    minos 10.13
      sdk 10.13
   ntools 0

running the program on any macOS from 11.1 up to 13.3 will print

        ...
	<key>ProductUserVisibleVersion</key>
	<string>10.16</string>
	<key>ProductVersion</key>
	<string>10.16</string>
       ...

instead of the true contents. Taking the same code and compiling it with GOOS=darwin GOARCH=arm64 go build example.go for Apple Silicon instead gives

% vtool -show-build example
example:
Load command 6
      cmd LC_BUILD_VERSION
  cmdsize 24
 platform MACOS
    minos 11.0
      sdk 11.0
   ntools 0

and running it on, say macOS 13.2.1:

        ...
	<key>ProductUserVisibleVersion</key>
	<string>13.2.1</string>
	<key>ProductVersion</key>
	<string>13.2.1</string>
        ...

In our case we ended up lipo-ing the amd64 and arm64 binaries together to get a universal binary where each slice specified a different sdk target version, and so the resulting binary behaved differently on different arches and even behaved differently on arm64 depending on whether or not it was running under Rosetta.

We found an acceptable workaround for this particular example, but all of this is mostly to say that things are kind of weird and it would be nice to be able to easily set the sdk of all macOS builds to the same version >= 11.0 from the Go linker itself to avoid any unexpected future Apple weirdness.

@thanm
Copy link
Contributor

thanm commented Mar 2, 2023

Looking at https://eclecticlight.co/2020/08/13/macos-version-numbering-isnt-so-simple, it sounds as though you can just change your program to exec the equivalent of

SYSTEM_VERSION_COMPAT=0 cat /System/Library/CoreServices/SystemVersion.plist

then read and interpret the result. Seems like that would solve your problem without needing any changes to the Go linker.

@mknyszek mknyszek added the WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. label Mar 8, 2023
@nguyen-phillip
Copy link
Author

We have a workaround for the specific example provided; I'm more interested in having a general solution. It makes a bit more sense to me to be able to enforce that our binaries target a recent SDK as Apple seems to expect, rather than finding new workarounds for each thing that might break because of this. I tried to find some other instances of where this might have been an issue, but the only thing that's come up is #30488, where the hardcoded value was breaking notarization.

If the recommended solution is to make a second pass over the binary with an external tool, I suppose that is fine, though not ideal from my perspective. I understand the reluctance to have an exploding list of platform-specific linker options.

@seankhliao seankhliao removed the WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. label Jul 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler/runtime Issues related to the Go compiler and/or runtime. NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Projects
Development

No branches or pull requests

8 participants