mirror of
https://github.com/nvm-sh/nvm.git
synced 2026-08-25 00:00:22 +08:00
[Robustness] nvm install: reject a structurally broken installed version
`nvm_is_version_installed` only checks that `bin/node` has the execute bit, which a zero-byte binary and a dangling `npm` symlink both pass, so a partial install could be reported as a success — and a broken existing version could short-circuit `nvm install` as "already installed". Add `nvm_validate_install`, which requires a non-empty `bin/node` and an `npm` entry that resolves, and use it in two places: gate the "already installed" shortcut on it, so a broken version is reinstalled rather than reused, and re-check it after an install reports success, so a broken result fails loudly instead of being activated. It checks layout, not execution: a correctly installed binary can still fail to run on an incompatible host (e.g. a newer node on an older glibc), which is not a broken install, and a corrupt download is already rejected by the checksum check before extraction.
This commit is contained in:
@@ -218,6 +218,52 @@ nvm_is_version_installed() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Sanity-check an installed version's layout: a non-empty, executable bin/node
|
||||||
|
# and, if present, an npm entry that resolves. nvm_is_version_installed only
|
||||||
|
# tests the bin/node exec bit, which a zero-byte binary and a dangling npm
|
||||||
|
# symlink both pass; this catches those, so a partial install is not reported
|
||||||
|
# as a success. It deliberately does NOT execute node: a correctly installed
|
||||||
|
# binary can still fail to run on an incompatible host (e.g. a newer node on an
|
||||||
|
# older glibc), which is not a broken install - and a corrupt download is
|
||||||
|
# already rejected by the checksum check before extraction.
|
||||||
|
nvm_validate_install() {
|
||||||
|
local VERSION
|
||||||
|
VERSION="${1-}"
|
||||||
|
if [ -z "${VERSION}" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local VERSION_PATH
|
||||||
|
VERSION_PATH="$(nvm_version_path "${VERSION}" 2>/dev/null)"
|
||||||
|
if [ -z "${VERSION_PATH}" ] || [ ! -d "${VERSION_PATH}" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local NVM_NODE_BINARY
|
||||||
|
NVM_NODE_BINARY='node'
|
||||||
|
if [ "_$(nvm_get_os)" = '_win' ]; then
|
||||||
|
NVM_NODE_BINARY='node.exe'
|
||||||
|
fi
|
||||||
|
|
||||||
|
local NVM_NODE_PATH
|
||||||
|
NVM_NODE_PATH="${VERSION_PATH}/bin/${NVM_NODE_BINARY}"
|
||||||
|
# A zero-byte file with the exec bit set still passes `[ -x ]` (the shell
|
||||||
|
# would run it as an empty script), so require a non-empty executable.
|
||||||
|
if [ ! -s "${NVM_NODE_PATH}" ] || [ ! -x "${NVM_NODE_PATH}" ]; then
|
||||||
|
nvm_err "The installed node binary at ${NVM_NODE_PATH} is missing or empty."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# npm ships with every supported node/io.js version; if its entry is a
|
||||||
|
# symlink it must resolve (one pointing at a removed target counts as broken).
|
||||||
|
if [ -h "${VERSION_PATH}/bin/npm" ] && [ ! -e "${VERSION_PATH}/bin/npm" ]; then
|
||||||
|
nvm_err "npm for ${VERSION} is a dangling symlink."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
nvm_print_npm_version() {
|
nvm_print_npm_version() {
|
||||||
if nvm_has "npm"; then
|
if nvm_has "npm"; then
|
||||||
local NPM_VERSION
|
local NPM_VERSION
|
||||||
@@ -3817,7 +3863,7 @@ nvm() {
|
|||||||
|
|
||||||
EXIT_CODE=0
|
EXIT_CODE=0
|
||||||
|
|
||||||
if nvm_is_version_installed "${VERSION}"; then
|
if nvm_is_version_installed "${VERSION}" && nvm_validate_install "${VERSION}"; then
|
||||||
nvm_err "${VERSION} is already installed."
|
nvm_err "${VERSION} is already installed."
|
||||||
nvm use "${VERSION}"
|
nvm use "${VERSION}"
|
||||||
EXIT_CODE=$?
|
EXIT_CODE=$?
|
||||||
@@ -3919,6 +3965,11 @@ nvm() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ $EXIT_CODE -eq 0 ] && ! nvm_validate_install "${VERSION}"; then
|
||||||
|
nvm_err "The install of ${VERSION} reported success but failed verification; not activating it."
|
||||||
|
EXIT_CODE=1
|
||||||
|
fi
|
||||||
|
|
||||||
if [ $EXIT_CODE -eq 0 ]; then
|
if [ $EXIT_CODE -eq 0 ]; then
|
||||||
if nvm_use_if_needed "${VERSION}" && nvm_install_npm_if_needed "${VERSION}"; then
|
if nvm_use_if_needed "${VERSION}" && nvm_install_npm_if_needed "${VERSION}"; then
|
||||||
if [ -n "${LTS-}" ]; then
|
if [ -n "${LTS-}" ]; then
|
||||||
@@ -4789,7 +4840,7 @@ nvm() {
|
|||||||
nvm_echo nvm_err nvm_grep nvm_cd \
|
nvm_echo nvm_err nvm_grep nvm_cd \
|
||||||
nvm_die_on_prefix nvm_get_make_jobs nvm_get_minor_version \
|
nvm_die_on_prefix nvm_get_make_jobs nvm_get_minor_version \
|
||||||
nvm_has_solaris_binary nvm_is_merged_node_version \
|
nvm_has_solaris_binary nvm_is_merged_node_version \
|
||||||
nvm_is_natural_num nvm_is_version_installed \
|
nvm_is_natural_num nvm_is_version_installed nvm_validate_install \
|
||||||
nvm_list_aliases nvm_make_alias nvm_print_alias_path \
|
nvm_list_aliases nvm_make_alias nvm_print_alias_path \
|
||||||
nvm_print_default_alias nvm_print_formatted_alias nvm_resolve_local_alias \
|
nvm_print_default_alias nvm_print_formatted_alias nvm_resolve_local_alias \
|
||||||
nvm_sanitize_path nvm_has_colors nvm_process_parameters \
|
nvm_sanitize_path nvm_has_colors nvm_process_parameters \
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
[ -n "${NVM_DIR}" ] && [ -d "${NVM_DIR}" ] && rm -rf "${NVM_DIR}"
|
||||||
|
unset -f die cleanup make_healthy make_broken \
|
||||||
|
nvm_remote_version nvm_has_executable nvm_binary_available \
|
||||||
|
nvm_install_binary nvm_install_source nvm_use_if_needed \
|
||||||
|
nvm_install_npm_if_needed nvm_install_default_packages nvm_ensure_default_set
|
||||||
|
unset NVM_DIR version version_path REINSTALL_MARKER OUTPUT EXIT_CODE
|
||||||
|
}
|
||||||
|
|
||||||
|
die() { echo "$@" ; cleanup ; exit 1; }
|
||||||
|
|
||||||
|
: nvm.sh
|
||||||
|
\. ../../../nvm.sh
|
||||||
|
|
||||||
|
unset npm_config_prefix NPM_CONFIG_PREFIX
|
||||||
|
|
||||||
|
NVM_DIR="$(mktemp -d)"
|
||||||
|
[ -n "${NVM_DIR}" ] || die 'unable to create temp NVM_DIR'
|
||||||
|
|
||||||
|
version='v20.0.0'
|
||||||
|
version_path="${NVM_DIR}/versions/node/${version}"
|
||||||
|
REINSTALL_MARKER="${NVM_DIR}/reinstalled"
|
||||||
|
|
||||||
|
# Stay offline and keep the post-install activation path inert; only the
|
||||||
|
# verification behaviour is under test here.
|
||||||
|
nvm_remote_version() { nvm_echo "${version}"; }
|
||||||
|
nvm_has_executable() { return 0; }
|
||||||
|
nvm_binary_available() { return 0; }
|
||||||
|
nvm_install_source() { return 1; }
|
||||||
|
nvm_use_if_needed() { return 0; }
|
||||||
|
nvm_install_npm_if_needed() { return 0; }
|
||||||
|
nvm_install_default_packages() { return 0; }
|
||||||
|
nvm_ensure_default_set() { return 0; }
|
||||||
|
|
||||||
|
make_healthy() {
|
||||||
|
mkdir -p "${version_path}/bin"
|
||||||
|
printf '#!/bin/sh\necho "%s"\n' "${version}" > "${version_path}/bin/node"
|
||||||
|
chmod +x "${version_path}/bin/node"
|
||||||
|
}
|
||||||
|
make_broken() {
|
||||||
|
mkdir -p "${version_path}/bin"
|
||||||
|
: > "${version_path}/bin/node" # zero-byte but +x: passes nvm_is_version_installed
|
||||||
|
chmod +x "${version_path}/bin/node"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1) An install that reports success but leaves a broken node must fail loudly
|
||||||
|
# and must not be activated.
|
||||||
|
rm -rf "${version_path}"
|
||||||
|
nvm_install_binary() { make_broken; return 0; }
|
||||||
|
OUTPUT="$(nvm install -b "${version}" 2>&1)"; EXIT_CODE=$?
|
||||||
|
[ "${EXIT_CODE}" != "0" ] || die "a broken install should fail; got exit 0, output >${OUTPUT}<"
|
||||||
|
case "${OUTPUT}" in
|
||||||
|
*'failed verification'*) ;;
|
||||||
|
*) die "expected a verification-failure message; got >${OUTPUT}<" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 2) An already-present but broken version must not short-circuit as "already
|
||||||
|
# installed"; it must be reinstalled.
|
||||||
|
rm -rf "${version_path}"
|
||||||
|
make_broken
|
||||||
|
nvm_install_binary() { command touch "${REINSTALL_MARKER}"; make_healthy; return 0; }
|
||||||
|
OUTPUT="$(nvm install -b "${version}" 2>&1)"; EXIT_CODE=$?
|
||||||
|
case "${OUTPUT}" in
|
||||||
|
*'is already installed'*) die "a broken version should not be treated as already installed: >${OUTPUT}<" ;;
|
||||||
|
esac
|
||||||
|
[ -f "${REINSTALL_MARKER}" ] || die 'a broken already-present version was not reinstalled'
|
||||||
|
[ "${EXIT_CODE}" = "0" ] || die "the healing reinstall should succeed; got ${EXIT_CODE}, output >${OUTPUT}<"
|
||||||
|
|
||||||
|
cleanup
|
||||||
Executable
+53
@@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
[ -n "${NVM_DIR}" ] && [ -d "${NVM_DIR}" ] && rm -rf "${NVM_DIR}"
|
||||||
|
unset -f die cleanup
|
||||||
|
unset NVM_DIR version version_path
|
||||||
|
}
|
||||||
|
|
||||||
|
die() { echo "$@" ; cleanup ; exit 1; }
|
||||||
|
|
||||||
|
: nvm.sh
|
||||||
|
\. ../../../nvm.sh
|
||||||
|
|
||||||
|
type nvm_validate_install > /dev/null 2>&1 || die 'nvm_validate_install is not available'
|
||||||
|
|
||||||
|
NVM_DIR="$(mktemp -d)"
|
||||||
|
[ -n "${NVM_DIR}" ] || die 'Unable to create temporary folder'
|
||||||
|
|
||||||
|
version='v20.0.0'
|
||||||
|
version_path="${NVM_DIR}/versions/node/${version}"
|
||||||
|
|
||||||
|
# No version directory at all: not valid.
|
||||||
|
nvm_validate_install "${version}" 2>/dev/null && die 'a missing version dir should not validate'
|
||||||
|
|
||||||
|
# A healthy install: a non-empty node plus an npm symlink that resolves.
|
||||||
|
# (validation checks layout, not execution, so bin/node need not be a real
|
||||||
|
# runnable binary here - just non-empty and executable.)
|
||||||
|
mkdir -p "${version_path}/bin" "${version_path}/lib/node_modules/npm/bin" || die 'setup mkdir failed'
|
||||||
|
printf '#!/bin/sh\necho "%s"\n' "${version}" > "${version_path}/bin/node"
|
||||||
|
chmod +x "${version_path}/bin/node"
|
||||||
|
echo 'npm-cli' > "${version_path}/lib/node_modules/npm/bin/npm-cli.js"
|
||||||
|
( cd "${version_path}/bin" && ln -s ../lib/node_modules/npm/bin/npm-cli.js npm )
|
||||||
|
nvm_validate_install "${version}" 2>/dev/null || die 'a healthy install should validate'
|
||||||
|
|
||||||
|
# A binary that is correctly installed but cannot run on this host (e.g. a
|
||||||
|
# newer node on an older glibc) must STILL validate: whether it runs is the
|
||||||
|
# host's concern, not a broken install.
|
||||||
|
printf '#!/bin/sh\nexit 1\n' > "${version_path}/bin/node"
|
||||||
|
chmod +x "${version_path}/bin/node"
|
||||||
|
nvm_validate_install "${version}" 2>/dev/null || die 'a non-runnable but present binary should still validate'
|
||||||
|
|
||||||
|
# A zero-byte but executable node: passes `[ -x ]` but must not validate.
|
||||||
|
: > "${version_path}/bin/node"
|
||||||
|
chmod +x "${version_path}/bin/node"
|
||||||
|
nvm_validate_install "${version}" 2>/dev/null && die 'a zero-byte node should not validate'
|
||||||
|
|
||||||
|
# A non-empty node again, but with a dangling npm symlink: must not validate.
|
||||||
|
printf '#!/bin/sh\necho "%s"\n' "${version}" > "${version_path}/bin/node"
|
||||||
|
chmod +x "${version_path}/bin/node"
|
||||||
|
rm -f "${version_path}/lib/node_modules/npm/bin/npm-cli.js"
|
||||||
|
nvm_validate_install "${version}" 2>/dev/null && die 'a dangling npm symlink should not validate'
|
||||||
|
|
||||||
|
cleanup
|
||||||
Reference in New Issue
Block a user