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

fix: add helper plugin to keep paths within sandbox #160

Merged
merged 8 commits into from
Jan 4, 2024
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: 7 additions & 4 deletions docs/esbuild.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion esbuild/private/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ exports_files(
)

exports_files(
["launcher.js"],
[
"launcher.js",
"plugins/bazel-sandbox.js",
],
visibility = ["//visibility:public"],
)

Expand Down
42 changes: 33 additions & 9 deletions esbuild/private/esbuild.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

load("@aspect_bazel_lib//lib:expand_make_vars.bzl", "expand_variables")
load("@aspect_bazel_lib//lib:copy_to_bin.bzl", "COPY_FILE_TO_BIN_TOOLCHAINS", "copy_file_to_bin_action", "copy_files_to_bin_actions")
load("@aspect_rules_js//js:libs.bzl", "js_lib_helpers")
load("@aspect_rules_js//js:libs.bzl", "js_lib_constants", "js_lib_helpers")
load("@aspect_rules_js//js:providers.bzl", "JsInfo", "js_info")
load(":helpers.bzl", "desugar_entry_point_names", "write_args_file")

Expand Down Expand Up @@ -175,6 +175,30 @@ See https://esbuild.github.io/api/#target for more details
See https://esbuild.github.io/api/#tsconfig for more details
""",
),
"bazel_sandbox_plugin": attr.bool(
default = True,
doc = """If true, a custom bazel-sandbox plugin will be enabled that prevents esbuild from leaving the Bazel sandbox.
See https://github.com/aspect-build/rules_esbuild/pull/160 for more info.""",
),
"esbuild_log_level": attr.string(
default = "warning",
doc = """Set the logging level of esbuild.

We set a default of "warmning" since the esbuild default of "info" includes
an output file summary which is slightly redundant under Bazel and may lead
to spammy `bazel build` output.

See https://esbuild.github.io/api/#log-level for more details.
""",
values = ["silent", "error", "warning", "info", "debug", "verbose"],
),
"js_log_level": attr.string(
default = "error",
doc = """Set the logging level for js_binary launcher and the JavaScript bazel-sandbox plugin.

Log levels: {}""".format(", ".join(js_lib_constants.LOG_LEVELS.keys())),
values = js_lib_constants.LOG_LEVELS.keys(),
),
}

def _bin_relative_path(ctx, file):
Expand Down Expand Up @@ -204,23 +228,17 @@ def _esbuild_impl(ctx):
]
for k, v in ctx.attr.define.items()
]),
# the entry point files to bundle
"entryPoints": [_bin_relative_path(ctx, entry_point) for entry_point in entry_points_bin_copy],
"external": ctx.attr.external,
# by default the log level is "info" and includes an output file summary
# under bazel this is slightly redundant and may lead to spammy logs
# Also disable the log limit and show all logs
"logLevel": "warning",
"logLevel": ctx.attr.esbuild_log_level,
# Disable the log limit and show all logs
"logLimit": 0,
"tsconfig": _bin_relative_path(ctx, tsconfig_bin_copy),
"metafile": ctx.attr.metafile,
"platform": ctx.attr.platform,
# Don't preserve symlinks since doing so breaks node_modules resolution
# in the pnpm-style symlinked node_modules structure.
# See https://pnpm.io/symlinked-node-modules-structure.
# NB: esbuild will currently leave the sandbox and end up in the output
# tree until symlink guards are created to prevent this.
# See https://github.com/aspect-build/rules_esbuild/pull/32.
"preserveSymlinks": False,
"sourcesContent": ctx.attr.sources_content,
"target": ctx.attr.target,
Expand Down Expand Up @@ -280,9 +298,15 @@ def _esbuild_impl(ctx):
"ESBUILD_BINARY_PATH": "../../../" + esbuild_toolinfo.target_tool_path,
}

if ctx.attr.bazel_sandbox_plugin:
env["ESBUILD_BAZEL_SANDBOX_PLUGIN"] = "1"

if ctx.attr.max_threads > 0:
env["GOMAXPROCS"] = str(ctx.attr.max_threads)

for log_level_env in js_lib_helpers.envs_for_log_level(ctx.attr.js_log_level):
env[log_level_env] = "1"

execution_requirements = {}
if "no-remote-exec" in ctx.attr.tags:
execution_requirements = {"no-remote-exec": "1"}
Expand Down
13 changes: 12 additions & 1 deletion esbuild/private/launcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { readFileSync, writeFileSync } = require('fs')
const { pathToFileURL } = require('url')
const { join } = require('path')
const esbuild = require('esbuild')
const { bazelSandboxPlugin } = require('./plugins/bazel-sandbox.js')

function getFlag(flag, required = true) {
const argvFlag = process.argv.find((arg) => arg.startsWith(`${flag}=`))
Expand Down Expand Up @@ -68,7 +69,7 @@ async function processConfigFile(configFilePath, existingArgs = {}) {

if (IGNORED_CONFIG_KEYS.includes(key)) {
console.error(
`[WARNING] esbuild configuration property '${key}' from '${configFilePath}' will be ignored and overriden`
`[WARNING] esbuild configuration property '${key}' from '${configFilePath}' will be ignored and overridden`
)
} else if (
MERGE_CONFIG_KEYS.includes(key) &&
Expand Down Expand Up @@ -117,6 +118,16 @@ async function runOneBuild(args, userArgsFilePath, configFilePath) {
}
}

const plugins = []
if (!!process.env.ESBUILD_BAZEL_SANDBOX_PLUGIN) {
// onResolve plugin, must be first to occur.
plugins.push(bazelSandboxPlugin())
}
if (args.plugins !== undefined) {
plugins.push(...args.plugins)
}
args.plugins = plugins

try {
const result = await esbuild.build(args)
if (result.metafile) {
Expand Down
88 changes: 88 additions & 0 deletions esbuild/private/plugins/bazel-sandbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const path = require('path')
const process = require('process')

// Regex matching any non-relative import path
const pkgImport = /^[^.]/

const bindir = process.env.BAZEL_BINDIR
const execroot = process.env.JS_BINARY__EXECROOT

// Under Bazel, esbuild will follow symlinks out of the sandbox when the sandbox is enabled. See https://github.com/aspect-build/rules_esbuild/issues/58.
// This plugin using a separate resolver to detect if the the resolution has left the execroot (which is the root of the sandbox
// when sandboxing is enabled) and patches the resolution back into the sandbox.
function bazelSandboxPlugin() {
return {
name: 'bazel-sandbox',
setup(build) {
const moduleCache = new Map()
build.onResolve(
{ filter: /./ },
async ({ path: importPath, ...otherOptions }) => {
// NB: these lines are to prevent infinite recursion when we call `build.resolve`.
if (otherOptions.pluginData) {
if (otherOptions.pluginData.executedSandboxPlugin) {
return
}
} else {
otherOptions.pluginData = {}
}
otherOptions.pluginData.executedSandboxPlugin = true

// Prevent us from loading different forms of a module (CJS vs ESM).
if (pkgImport.test(importPath)) {
if (!moduleCache.has(importPath)) {
moduleCache.set(
importPath,
resolveInExecroot(build, importPath, otherOptions)
)
}
return await moduleCache.get(importPath)
}
return await resolveInExecroot(build, importPath, otherOptions)
}
)
},
}
}

async function resolveInExecroot(build, importPath, otherOptions) {
const result = await build.resolve(importPath, otherOptions)

if (result.errors && result.errors.length) {
// There was an error resolving, just return the error as-is.
return result
}

if (
!result.path.startsWith('.') &&
!result.path.startsWith('/') &&
!result.path.startsWith('\\')
) {
// Not a relative or absolute path. Likely a module resolution that is marked "external"
return result
}

// If esbuild attempts to leave the execroot, map the path back into the execroot.
if (!result.path.startsWith(execroot)) {
// If it tried to leave bazel-bin, error out completely.
if (!result.path.includes(bindir)) {
throw new Error(
`Error: esbuild resolved a path outside of BAZEL_BINDIR (${bindir}): ${result.path}`
)
}
// Otherwise remap the bindir-relative path
const correctedPath = path.join(
execroot,
result.path.substring(result.path.indexOf(bindir))
)
if (!!process.env.JS_BINARY__LOG_DEBUG) {
console.error(
`DEBUG: [bazel-sandbox] correcting esbuild resolution ${result.path} that left the sandbox to ${correctedPath}.`
)
}
result.path = correctedPath
}
return result
}

module.exports = { bazelSandboxPlugin }
9 changes: 8 additions & 1 deletion esbuild/repositories.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ def _esbuild_repo_impl(repository_ctx):
Label("@aspect_rules_esbuild//esbuild/private:launcher.js"),
"launcher.js",
)
repository_ctx.symlink(
Label("@aspect_rules_esbuild//esbuild/private:plugins/bazel-sandbox.js"),
"plugins/bazel-sandbox.js",
)
build_content = """#Generated by esbuild/repositories.bzl
load("@aspect_rules_esbuild//esbuild:toolchain.bzl", "esbuild_toolchain")
load("@aspect_rules_js//js:defs.bzl", "js_binary")
Expand All @@ -56,7 +60,10 @@ npm_link_package(
js_binary(
name = "launcher",
entry_point = "launcher.js",
data = [":node_modules/esbuild"],
data = [
":plugins/bazel-sandbox.js",
":node_modules/esbuild",
],
)

esbuild_toolchain(
Expand Down
3 changes: 3 additions & 0 deletions examples/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ esbuild(
"main.js",
"name.js",
],
bazel_sandbox_plugin = True,
entry_point = "main.js",
esbuild_log_level = "verbose",
js_log_level = "debug",
metafile = True,
)

Expand Down