Skip to content

Commit

Permalink
fix: add helper plugin to keep paths within sandbox (#160)
Browse files Browse the repository at this point in the history
* fix: add helper plugin to keep paths within sandbox

* respond to comments, attempt to incorporate into launcher.js

* respond to PR review

* make non-ESM and fix paths

* remove semicolon

* rename plugin to "bazel-sandbox", add an opt-out parameter and add debug logs and the ability to set the JS and esbuild log levels

* Update docs

* Handle external module resolutions

---------

Co-authored-by: Greg Magolan <[email protected]>
  • Loading branch information
vpanta and gregmagolan authored Jan 4, 2024
1 parent 4f9895f commit e39ac34
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 16 deletions.
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

0 comments on commit e39ac34

Please sign in to comment.