Skip to content

Commit 4e57ebc

Browse files
justintrudellmeta-codesync[bot]
authored andcommitted
[antlir2][unprivileged_dir] add option for buck-out safe encoding
Summary: `buck2` has rules against certain filename patterns, so this adds an optional base64 filename encoding feature behind a boolean flag. We only escape paths that require escaping, and we track those that have been escaped in a JSON map so they can be reconstructed where needed (e.g. during CAF upload). This required some refactoring as I needed to move unprivileged_dir to a custom impl rather than leaving it in defs.bzl, given I needed to emit a subtarget. I extracted the shared attrs to a separate attrs.bzl file so they can be imported without circular deps. Test Plan: ``` $ buck2 test mode/opt fbcode//antlir/antlir2/test_images/package/unprivileged_dir/... ``` https://www.internalfb.com/intern/testinfra/testrun/6473924782953215 ``` $ buck2 test mode/opt fbcode//antlir/antlir2/test_images/package/unprivileged_dir/... ``` https://www.internalfb.com/intern/testinfra/testrun/2251800148706544 Reviewed By: vmagro Differential Revision: D88997743 fbshipit-source-id: e1d1266263534452aedaf87c77196e0bd5c321e1
1 parent fb697c0 commit 4e57ebc

File tree

11 files changed

+336
-42
lines changed

11 files changed

+336
-42
lines changed

antlir/antlir2/antlir2_packager/BUCK

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ rust_binary(
1717
visibility = ["PUBLIC"],
1818
deps = [
1919
"anyhow",
20+
"base64",
2021
"blake3",
2122
"bytesize",
2223
"cap-std",

antlir/antlir2/antlir2_packager/src/unprivileged_dir.rs

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,32 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
use std::collections::BTreeMap;
89
use std::fs::Permissions;
10+
use std::os::unix::ffi::OsStrExt;
911
use std::os::unix::fs::MetadataExt;
1012
use std::os::unix::fs::PermissionsExt;
1113
use std::path::Path;
14+
use std::path::PathBuf;
1215

1316
use anyhow::Context;
1417
use anyhow::Result;
18+
use base64::Engine as _;
19+
use base64::engine::general_purpose::URL_SAFE;
1520
use serde::Deserialize;
1621
use walkdir::WalkDir;
1722

23+
/// Check if a filename contains characters that Buck2 doesn't allow.
24+
/// Buck2 disallows forward slash '/' and backslash '\' in filenames.
25+
fn needs_escaping(component: &std::ffi::OsStr) -> bool {
26+
component.as_bytes().contains(&b'\\')
27+
}
28+
1829
#[derive(Debug, Clone, Deserialize)]
1930
#[serde(deny_unknown_fields)]
20-
pub struct UnprivilegedDir {}
31+
pub struct UnprivilegedDir {
32+
base64_encoded_filenames: Option<PathBuf>,
33+
}
2134

2235
impl UnprivilegedDir {
2336
pub(crate) fn build(
@@ -27,7 +40,13 @@ impl UnprivilegedDir {
2740
root_guard: Option<antlir2_rootless::EscalationGuard>,
2841
) -> Result<()> {
2942
let layer = layer.canonicalize()?;
43+
44+
// Track escaped paths: escaped_relative_path -> original_relative_path
45+
// such that they can be reconstructed by consumers of the dir
46+
let mut escaped_paths: BTreeMap<PathBuf, PathBuf> = BTreeMap::new();
47+
3048
std::fs::create_dir(out).context("while creating root")?;
49+
3150
std::os::unix::fs::lchown(
3251
out,
3352
root_guard
@@ -46,7 +65,29 @@ impl UnprivilegedDir {
4665
if relpath == Path::new("") {
4766
continue;
4867
}
49-
let dst = out.join(relpath);
68+
let dst = if self.base64_encoded_filenames.is_some() {
69+
let encoded_path = relpath
70+
.components()
71+
.map(|component| {
72+
let component_os = component.as_os_str();
73+
if needs_escaping(component_os) {
74+
PathBuf::from(URL_SAFE.encode(component_os.as_bytes()))
75+
} else {
76+
PathBuf::from(component_os)
77+
}
78+
})
79+
.collect::<PathBuf>();
80+
if encoded_path != relpath {
81+
// Ensure the mapping always contains absolute paths
82+
escaped_paths.insert(
83+
PathBuf::from("/").join(&encoded_path),
84+
PathBuf::from("/").join(relpath),
85+
);
86+
}
87+
out.join(encoded_path)
88+
} else {
89+
out.join(relpath)
90+
};
5091
if entry.file_type().is_dir() {
5192
std::fs::create_dir(&dst)
5293
.with_context(|| format!("while creating directory '{}'", dst.display()))?;
@@ -56,8 +97,13 @@ impl UnprivilegedDir {
5697
std::os::unix::fs::symlink(target, &dst)
5798
.with_context(|| format!("while creating symlink '{}'", dst.display()))?;
5899
} else if entry.file_type().is_file() {
59-
std::fs::copy(entry.path(), &dst)
60-
.with_context(|| format!("while copying file '{}'", dst.display()))?;
100+
std::fs::copy(entry.path(), &dst).with_context(|| {
101+
format!(
102+
"while copying file '{}' -> '{}'",
103+
entry.path().display(),
104+
dst.display()
105+
)
106+
})?;
61107
let mut mode = entry.metadata()?.mode();
62108
// preserve executable bit
63109
if (mode & 0o111) != 0 {
@@ -82,6 +128,17 @@ impl UnprivilegedDir {
82128
)
83129
.with_context(|| format!("while chowning '{}'", dst.display()))?;
84130
}
131+
132+
if let Some(base64_encoded_filenames) = &self.base64_encoded_filenames {
133+
std::fs::write(
134+
base64_encoded_filenames,
135+
serde_json::to_string_pretty(&escaped_paths)
136+
.context("while serializing escaped paths mapping")?
137+
.as_bytes(),
138+
)
139+
.context("while writing escaped paths mapping")?;
140+
}
141+
85142
Ok(())
86143
}
87144
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#
3+
# This source code is licensed under the MIT license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
load("//antlir/antlir2/bzl:platform.bzl", "arch_select")
7+
load("//antlir/antlir2/bzl/image:cfg.bzl", "attrs_selected_by_cfg")
8+
load("//antlir/antlir2/features:defs.bzl", "FeaturePluginInfo", "FeaturePluginPluginKind")
9+
load(":cfg.bzl", "layer_attrs")
10+
11+
# Attrs that are required by all packages
12+
common_attrs = {
13+
"labels": attrs.list(attrs.string(), default = []),
14+
"out": attrs.option(attrs.string(doc = "Output filename"), default = None),
15+
"_plugins": attrs.list(
16+
attrs.dep(providers = [FeaturePluginInfo]),
17+
default = [],
18+
doc = "Used as a way to pass plugins to anon layer targets",
19+
),
20+
} | layer_attrs
21+
22+
# Attrs that are not expected for users to pass
23+
default_attrs = {
24+
"_analyze_feature": attrs.exec_dep(default = "antlir//antlir/antlir2/antlir2_depgraph_if:analyze"),
25+
"_antlir2": attrs.exec_dep(default = "antlir//antlir/antlir2/antlir2:antlir2"),
26+
"_antlir2_packager": attrs.default_only(attrs.exec_dep(default = "antlir//antlir/antlir2/antlir2_packager:antlir2-packager")),
27+
"_dot_meta_feature": attrs.dep(default = "antlir//antlir/antlir2/bzl/package:dot-meta", pulls_plugins = [FeaturePluginPluginKind]),
28+
"_run_container": attrs.exec_dep(default = "antlir//antlir/antlir2/container_subtarget:run"),
29+
"_target_arch": attrs.default_only(attrs.string(
30+
default = arch_select(aarch64 = "aarch64", x86_64 = "x86_64"),
31+
)),
32+
} | attrs_selected_by_cfg()

antlir/antlir2/bzl/package/defs.bzl

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,17 @@
55

66
load("//antlir/antlir2/bzl:platform.bzl", "arch_select", "os_select")
77
load("//antlir/antlir2/bzl:types.bzl", "BuildApplianceInfo", "LayerInfo")
8-
load("//antlir/antlir2/bzl/image:cfg.bzl", "attrs_selected_by_cfg")
9-
load("//antlir/antlir2/features:defs.bzl", "FeaturePluginInfo", "FeaturePluginPluginKind")
8+
load("//antlir/antlir2/features:defs.bzl", "FeaturePluginPluginKind")
109
load("//antlir/buck2/bzl:ensure_single_output.bzl", "ensure_single_output")
1110
load("//antlir/bzl:internal_external.bzl", "internal_external")
11+
load(":attrs.bzl", "common_attrs", "default_attrs")
1212
load(":btrfs.bzl", "btrfs")
13-
load(":cfg.bzl", "layer_attrs", "package_cfg")
13+
load(":cfg.bzl", "package_cfg")
1414
load(":gpt.bzl", "GptPartitionSource", "gpt")
1515
load(":macro.bzl", "package_macro")
1616
load(":sendstream.bzl", "sendstream_v2")
1717
load(":stamp_buildinfo.bzl", "stamp_buildinfo_rule")
18-
19-
# Attrs that are required by all packages
20-
common_attrs = {
21-
"labels": attrs.list(attrs.string(), default = []),
22-
"out": attrs.option(attrs.string(doc = "Output filename"), default = None),
23-
"_plugins": attrs.list(
24-
attrs.dep(providers = [FeaturePluginInfo]),
25-
default = [],
26-
doc = "Used as a way to pass plugins to anon layer targets",
27-
),
28-
} | layer_attrs
29-
30-
# Attrs that are not expected for users to pass
31-
default_attrs = {
32-
"_analyze_feature": attrs.exec_dep(default = "antlir//antlir/antlir2/antlir2_depgraph_if:analyze"),
33-
"_antlir2": attrs.exec_dep(default = "antlir//antlir/antlir2/antlir2:antlir2"),
34-
"_antlir2_packager": attrs.default_only(attrs.exec_dep(default = "antlir//antlir/antlir2/antlir2_packager:antlir2-packager")),
35-
"_dot_meta_feature": attrs.dep(default = "antlir//antlir/antlir2/bzl/package:dot-meta", pulls_plugins = [FeaturePluginPluginKind]),
36-
"_run_container": attrs.exec_dep(default = "antlir//antlir/antlir2/container_subtarget:run"),
37-
"_target_arch": attrs.default_only(attrs.string(
38-
default = arch_select(aarch64 = "aarch64", x86_64 = "x86_64"),
39-
)),
40-
} | attrs_selected_by_cfg()
18+
load(":unprivileged_dir.bzl", "unprivileged_dir")
4119

4220
def _generic_impl_with_layer(
4321
layer: [Dependency, ProviderCollection],
@@ -434,13 +412,6 @@ _ext4, _ext4_anon = _new_package_rule(
434412
uses_build_appliance = True,
435413
)
436414

437-
# @unused
438-
_unprivileged_dir, unprivileged_dir_anon = _new_package_rule(
439-
format = "unprivileged_dir",
440-
is_dir = True,
441-
sudo = True,
442-
)
443-
444415
# @unused
445416
_erofs, _erofs_anon = _new_package_rule(
446417
format = "erofs",
@@ -468,6 +439,6 @@ package = struct(
468439
tar = package_macro(_tar),
469440
tar_gz = package_macro(_tar_gz),
470441
tar_zst = package_macro(tar_zst_rule),
471-
unprivileged_dir = package_macro(_unprivileged_dir),
442+
unprivileged_dir = unprivileged_dir,
472443
vfat = package_macro(_vfat),
473444
)

antlir/antlir2/bzl/package/docker_archive.bzl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66
load("//antlir/antlir2/bzl:types.bzl", "BuildApplianceInfo", "LayerInfo")
77
load("//antlir/buck2/bzl:ensure_single_output.bzl", "ensure_single_output")
8+
load(":attrs.bzl", "common_attrs", "default_attrs")
89
load(":cfg.bzl", "layer_attrs", "package_cfg")
9-
load(":defs.bzl", "common_attrs", "default_attrs")
1010
load(":macro.bzl", "package_macro")
1111
load(":oci.bzl", "oci_attrs", "oci_rule")
1212

antlir/antlir2/bzl/package/oci.bzl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
# LICENSE file in the root directory of this source tree.
55

66
load("//antlir/antlir2/bzl:types.bzl", "LayerInfo")
7+
load(":attrs.bzl", "common_attrs", "default_attrs")
78
load(":cfg.bzl", "layer_attrs", "package_cfg")
8-
load(":defs.bzl", "common_attrs", "default_attrs")
99
load(":macro.bzl", "package_macro")
1010

1111
OciLayer = record(
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#
3+
# This source code is licensed under the MIT license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
load("//antlir/antlir2/bzl:types.bzl", "LayerInfo")
7+
load("//antlir/antlir2/features:defs.bzl", "FeaturePluginPluginKind")
8+
load("//antlir/buck2/bzl:ensure_single_output.bzl", "ensure_single_output")
9+
load(":attrs.bzl", "common_attrs", "default_attrs")
10+
load(":cfg.bzl", "package_cfg")
11+
load(":macro.bzl", "package_macro")
12+
load(":stamp_buildinfo.bzl", "stamp_buildinfo_rule")
13+
14+
def _unprivileged_dir_impl_with_layer(
15+
layer: [Dependency, ProviderCollection],
16+
*,
17+
ctx: AnalysisContext) -> list[Provider]:
18+
output_name = ctx.attrs.out or ctx.label.name
19+
package = ctx.actions.declare_output(output_name, dir = True)
20+
21+
encoded_path_mapping = ctx.actions.declare_output("encoded_path_mapping.json")
22+
if not ctx.attrs.base64_encode_filenames:
23+
ctx.actions.write_json(encoded_path_mapping.as_output(), {})
24+
spec = ctx.actions.write_json(
25+
"spec.json",
26+
{"unprivileged_dir": {
27+
"base64_encoded_filenames": encoded_path_mapping.as_output(),
28+
} if ctx.attrs.base64_encode_filenames else {}},
29+
with_inputs = True,
30+
)
31+
ctx.actions.run(
32+
cmd_args(
33+
cmd_args("sudo", "--preserve-env=TMPDIR") if not ctx.attrs._rootless else cmd_args(),
34+
ctx.attrs._antlir2_packager[RunInfo],
35+
cmd_args(spec, format = "--spec={}"),
36+
cmd_args(layer[LayerInfo].contents.subvol_symlink, format = "--layer={}"),
37+
"--dir",
38+
cmd_args(package.as_output(), format = "--out={}"),
39+
"--rootless" if ctx.attrs._rootless else cmd_args(),
40+
),
41+
local_only = True,
42+
category = "antlir2_package",
43+
identifier = "unprivileged_dir",
44+
)
45+
46+
return [DefaultInfo(package, sub_targets = {
47+
"base64_encoded_path_mapping": [DefaultInfo(encoded_path_mapping)],
48+
})]
49+
50+
def _unprivileged_dir_impl(ctx: AnalysisContext):
51+
if ctx.attrs.dot_meta:
52+
return ctx.actions.anon_target(stamp_buildinfo_rule, {
53+
"build_appliance": ctx.attrs.build_appliance,
54+
"flavor": ctx.attrs.flavor,
55+
"layer": ctx.attrs.layer,
56+
"name": str(ctx.label.raw_target()),
57+
"_analyze_feature": ctx.attrs._analyze_feature,
58+
"_antlir2": ctx.attrs._antlir2,
59+
"_dot_meta_feature": ctx.attrs._dot_meta_feature,
60+
"_plugins": ctx.attrs._plugins + (ctx.plugins[FeaturePluginPluginKind] if FeaturePluginPluginKind in ctx.plugins else []),
61+
"_rootless": ctx.attrs._rootless,
62+
"_run_container": ctx.attrs._run_container,
63+
"_target_arch": ctx.attrs._target_arch,
64+
"_working_format": ctx.attrs._working_format,
65+
}).promise.map(partial(
66+
_unprivileged_dir_impl_with_layer,
67+
ctx = ctx,
68+
))
69+
else:
70+
return _unprivileged_dir_impl_with_layer(
71+
layer = ctx.attrs.layer,
72+
ctx = ctx,
73+
)
74+
75+
_unprivileged_dir_attrs = {
76+
"base64_encode_filenames": attrs.bool(
77+
default = False,
78+
doc = "Encode filenames that contain invalid characters (/ or \\) in base64 so they are legal in buck-out",
79+
),
80+
"dot_meta": attrs.bool(default = True),
81+
}
82+
83+
_unprivileged_dir = rule(
84+
impl = _unprivileged_dir_impl,
85+
cfg = package_cfg,
86+
uses_plugins = [FeaturePluginPluginKind],
87+
attrs = default_attrs | common_attrs | _unprivileged_dir_attrs,
88+
)
89+
90+
unprivileged_dir_anon = anon_rule(
91+
impl = lambda ctx: _unprivileged_dir_impl_with_layer(ctx.attrs.layer, ctx = ctx),
92+
artifact_promise_mappings = {
93+
"base64_encoded_path_mapping": lambda x: ensure_single_output(x[DefaultInfo].sub_targets["base64_encoded_path_mapping"]),
94+
"package": lambda x: ensure_single_output(x),
95+
},
96+
attrs = default_attrs | common_attrs | _unprivileged_dir_attrs,
97+
)
98+
99+
unprivileged_dir = package_macro(_unprivileged_dir)

antlir/antlir2/bzl/package/xar.bzl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
# LICENSE file in the root directory of this source tree.
55

66
load("//antlir/antlir2/features:defs.bzl", "FeaturePluginPluginKind")
7+
load(":attrs.bzl", "common_attrs", "default_attrs")
78
load(":cfg.bzl", "layer_attrs", "package_cfg")
8-
load(":defs.bzl", "common_attrs", "default_attrs", "squashfs_anon")
9+
load(":defs.bzl", "squashfs_anon")
910
load(":macro.bzl", "package_macro")
1011

1112
def _impl(ctx: AnalysisContext) -> list[Provider]:

0 commit comments

Comments
 (0)