Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/installer/activation.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package installer

import (
"common/acl"
"common/settings"
"common/system"
"common/verifycache"
Expand All @@ -25,6 +26,16 @@ func ActivateVersion(version string) error {
return fmt.Errorf("version must not be empty")
}

if allowed, err := acl.IsAllowedVersion(version); !allowed {
if err != nil {
return fmt.Errorf("NVM4411: %w", err)
}
return fmt.Errorf("NVM4411: v%s is blocked by version firewall", version)
}
if err := verifyNodeBinaryMatchesVersion(getRoot(version), version); err != nil {
return err
}

lastVersion, err := stringSetting("active_version")
if err != nil {
return err
Expand Down
149 changes: 149 additions & 0 deletions src/installer/verification.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,152 @@ func peCompanyName(exePath, fallback string) string {
}
return company
}

const vsFixedFileInfoSignature = 0xFEEF04BD

type vsFixedFileInfo struct {
Signature uint32
StrucVersion uint32
FileVersionMS uint32
FileVersionLS uint32
ProductVersionMS uint32
ProductVersionLS uint32
FileFlagsMask uint32
FileFlags uint32
FileOS uint32
FileType uint32
FileSubtype uint32
FileDateMS uint32
FileDateLS uint32
}

type peCoreVersion struct {
major uint32
minor uint32
patch uint32
}

func parseCoreVersion(raw string) (peCoreVersion, bool) {
s := strings.TrimSpace(raw)
if len(s) >= 1 && (s[0] == 'v' || s[0] == 'V') {
s = s[1:]
}
if s == "" {
return peCoreVersion{}, false
}
if i := strings.IndexAny(s, "-+"); i >= 0 {
s = s[:i]
}
parts := strings.Split(s, ".")
if len(parts) == 0 || parts[0] == "" {
return peCoreVersion{}, false
}
major, err := parseVersionPart(parts[0])
if err != nil {
return peCoreVersion{}, false
}
minor := uint32(0)
patch := uint32(0)
if len(parts) > 1 {
minor, err = parseVersionPart(parts[1])
if err != nil {
return peCoreVersion{}, false
}
}
if len(parts) > 2 {
patch, err = parseVersionPart(parts[2])
if err != nil {
return peCoreVersion{}, false
}
}
return peCoreVersion{major: major, minor: minor, patch: patch}, true
}

func parseVersionPart(raw string) (uint32, error) {
if raw == "" {
return 0, fmt.Errorf("empty")
}
var n uint64
for _, c := range raw {
if c < '0' || c > '9' {
return 0, fmt.Errorf("not a number")
}
n = n*10 + uint64(c-'0')
if n > 0xffffffff {
return 0, fmt.Errorf("overflow")
}
}
return uint32(n), nil
}

func coreVersionEqual(file peCoreVersion, expected string) bool {
folder, ok := parseCoreVersion(expected)
if !ok {
return false
}
return file.major == folder.major && file.minor == folder.minor && file.patch == folder.patch
}

func peFileVersion(exePath string) (peCoreVersion, error) {
vdll := windows.NewLazySystemDLL("version.dll")
getInfoSize := vdll.NewProc("GetFileVersionInfoSizeW")
getInfo := vdll.NewProc("GetFileVersionInfoW")
queryValue := vdll.NewProc("VerQueryValueW")

path, err := windows.UTF16PtrFromString(exePath)
if err != nil {
return peCoreVersion{}, err
}

size, _, _ := getInfoSize.Call(uintptr(unsafe.Pointer(path)), 0)
if size == 0 {
return peCoreVersion{}, fmt.Errorf("VERSIONINFO missing")
}

buf := make([]byte, size)
ret, _, _ := getInfo.Call(uintptr(unsafe.Pointer(path)), 0, size, uintptr(unsafe.Pointer(&buf[0])))
if ret == 0 {
return peCoreVersion{}, fmt.Errorf("VERSIONINFO missing")
}

rootBlock, err := windows.UTF16PtrFromString(`\`)
if err != nil {
return peCoreVersion{}, err
}
var infoPtr uintptr
var infoLen uint32
ret, _, _ = queryValue.Call(uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(rootBlock)), uintptr(unsafe.Pointer(&infoPtr)), uintptr(unsafe.Pointer(&infoLen)))
if ret == 0 || infoPtr == 0 || infoLen < uint32(unsafe.Sizeof(vsFixedFileInfo{})) {
return peCoreVersion{}, fmt.Errorf("VERSIONINFO missing")
}

info := (*vsFixedFileInfo)(unsafe.Pointer(infoPtr))
if info.Signature != vsFixedFileInfoSignature {
return peCoreVersion{}, fmt.Errorf("VERSIONINFO missing")
}

return peCoreVersion{
major: info.FileVersionMS >> 16,
minor: info.FileVersionMS & 0xffff,
patch: info.FileVersionLS >> 16,
}, nil
}

func verifyNodeBinaryMatchesVersion(versionDir, version string) error {
exePath := filepath.Join(versionDir, "node.exe")
if _, err := os.Stat(exePath); err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("NVM4314: unable to inspect %s: %w", exePath, err)
}

file, err := peFileVersion(exePath)
if err != nil {
return fmt.Errorf("NVM4314: unable to read FileVersion from %s: %w", exePath, err)
}
if !coreVersionEqual(file, version) {
return fmt.Errorf("NVM4314: node.exe FileVersion %d.%d.%d does not match folder v%s", file.major, file.minor, file.patch, version)
}
return nil
}
45 changes: 45 additions & 0 deletions src/installer/verification_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,48 @@ func TestVerifyNodeSHASUMFixtureRegression(t *testing.T) {
t.Fatal("verifyNodeSHASUM(fixture) = false, want true")
}
}

func TestParseCoreVersion(t *testing.T) {
got, ok := parseCoreVersion("v24.0.0")
if !ok || got != (peCoreVersion{major: 24, minor: 0, patch: 0}) {
t.Fatalf("parseCoreVersion(v24.0.0) = %#v ok=%v", got, ok)
}
got, ok = parseCoreVersion("24.0.0-nightly20240101")
if !ok || got != (peCoreVersion{major: 24, minor: 0, patch: 0}) {
t.Fatalf("parseCoreVersion(prerelease) = %#v ok=%v", got, ok)
}
if _, ok = parseCoreVersion(""); ok {
t.Fatal("parseCoreVersion(empty) ok, want false")
}
}

func TestCoreVersionEqualCatchesSwap(t *testing.T) {
fileV10 := peCoreVersion{major: 10, minor: 0, patch: 0}
if coreVersionEqual(fileV10, "24.0.0") {
t.Fatal("Node 10 FileVersion must not match folder 24.0.0")
}
if !coreVersionEqual(fileV10, "10.0.0") {
t.Fatal("Node 10 FileVersion must match folder 10.0.0")
}
if !coreVersionEqual(peCoreVersion{major: 24, minor: 1, patch: 0}, "24.1.0-rc.1") {
t.Fatal("prerelease folder must compare on major.minor.patch")
}
}

func TestVerifyNodeBinaryMatchesVersionSkipsMissingExe(t *testing.T) {
dir := t.TempDir()
if err := verifyNodeBinaryMatchesVersion(dir, "24.0.0"); err != nil {
t.Fatalf("missing node.exe should skip: %v", err)
}
}

func TestVerifyNodeBinaryMatchesVersionRejectsNonPE(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "node.exe"), []byte("not a pe"), 0o644); err != nil {
t.Fatalf("WriteFile(node.exe) error = %v", err)
}
err := verifyNodeBinaryMatchesVersion(dir, "24.0.0")
if err == nil || !strings.Contains(err.Error(), "NVM4314") {
t.Fatalf("non-PE node.exe error = %v, want NVM4314", err)
}
}
Loading