Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ba55580
Port osmodifier from Go binary to native Rust crate
bfjelds May 11, 2026
9684c93
fix: correct module.options type (HashMap not Option) and remove unus…
bfjelds May 11, 2026
accd65e
fix: remove osmodifier binary from pipelines, Makefile, and functiona…
bfjelds May 11, 2026
30afe99
fix: restore Path import in osconfig and remove unused imports
bfjelds May 11, 2026
6303de6
fix: apply cargo fmt formatting
bfjelds May 11, 2026
e64322f
fix: resolve clippy redundant_closure warning
bfjelds May 11, 2026
0c7e5ad
fix: remove stale osmodifier option and constant from conftest.py
bfjelds May 12, 2026
dff2cc2
fix: address deep review findings - atomic writes, security, correctness
bfjelds May 12, 2026
7a25b62
fix: remove trailing newline from hostname write to match Go behavior
bfjelds May 12, 2026
23ca4c8
Add functional tests for osmodifier crate
bfjelds May 14, 2026
ee3bb10
fix: apply cargo fmt formatting corrections
bfjelds May 14, 2026
9399a1e
fix: update Cargo.lock with osmodifier test dependencies
bfjelds May 14, 2026
d7e563b
docs: add README mapping Rust port to Go source files
bfjelds May 16, 2026
afa8641
refactor: use Dependency enum for system tool invocations
bfjelds May 16, 2026
a97e604
simplify: remove chroot codepath from osmodifier
bfjelds May 16, 2026
691b577
fix: break cyclic dependency between osutils and osmodifier
bfjelds May 16, 2026
33fb451
fix: update Cargo.lock after removing osutils→osmodifier dependency
bfjelds May 16, 2026
af8c03b
fix: use full import path osutils::dependencies::Dependency
bfjelds May 16, 2026
cba55c5
fix: remove trailing blank lines (cargo fmt)
bfjelds May 16, 2026
0e2ab38
fix: cargo fmt and unused variable warning
bfjelds May 16, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,6 @@ stages:

- template: ../common_tasks/update-protoc.yml

- template: ../common_tasks/download-osmodifier.yml
parameters:
tridentSourceDirectory: $(TRIDENT_SOURCE_DIR)
osModifierBranch: ${{ parameters.osModifierBranch }}
targetArchitecture: amd64

- bash: |
set -eux

Expand Down
12 changes: 0 additions & 12 deletions .pipelines/templates/stages/trident_rpms/build-source.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,6 @@ stages:
- template: ../common_tasks/cargo-auth.yml
parameters:
cargoConfigPath: $(TRIDENT_SOURCE_DIR)/.cargo/config.toml
- template: ../common_tasks/download-osmodifier.yml
parameters:
tridentSourceDirectory: $(TRIDENT_SOURCE_DIR)
targetArchitecture: ${{ parameters.targetArchitecture }}
osModifierBranch: ${{ parameters.osModifierBranch }}
osModifierBuildType: ${{ parameters.osModifierBuildType }}
- template: release.yml
parameters:
targetArchitecture: ${{ parameters.targetArchitecture }}
Expand Down Expand Up @@ -144,12 +138,6 @@ stages:
set -eux
sudo systemctl start docker
displayName: Start Docker
- template: ../common_tasks/download-osmodifier.yml
parameters:
tridentSourceDirectory: $(TRIDENT_SOURCE_DIR)
targetArchitecture: ${{ parameters.targetArchitecture }}
osModifierBranch: ${{ parameters.osModifierBranch }}
osModifierBuildType: ${{ parameters.osModifierBuildType }}
- template: release.yml
parameters:
targetArchitecture: ${{ parameters.targetArchitecture }}
Expand Down
13 changes: 0 additions & 13 deletions .pipelines/templates/stages/validate_makefile/dev-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,19 +85,6 @@ stages:
steps:
- template: ../common_tasks/checkout_trident.yml
- template: ../common_tasks/avoid-pypi-usage.yml
- bash: |
set -eux
make artifacts/osmodifier
rm -rf artifacts/osmodifier
displayName: Invoke make artifacts/osmodifier
workingDirectory: $(TRIDENT_SOURCE_DIR)

- template: ../common_tasks/download-osmodifier.yml
parameters:
tridentSourceDirectory: $(TRIDENT_SOURCE_DIR)
osModifierBuildType: dev
osModifierBranch: ${{ parameters.osModifierBranch }}
targetArchitecture: amd64

- script: |
set -eux
Expand Down
19 changes: 19 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ default-members = ["crates/trident"]
members = [
"crates/docbuilder",
"crates/trident-acl-agent",
"crates/osmodifier",
"crates/osutils",
"crates/pytest_gen",
"crates/pytest",
Expand Down
33 changes: 6 additions & 27 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -142,25 +142,8 @@ target/release/trident target/release/trident-acl-agent: .cargo/config | version
TRIDENT_VERSION="$(LOCAL_BUILD_TRIDENT_VERSION)" \
cargo build --release --features dangerous-options,grpc-preview -p trident -p trident-acl-agent

TOOLKIT_DIR="azure-linux-image-tools/toolkit"
AZL_TOOLS_OUT_DIR="$(TOOLKIT_DIR)/out/tools"
ARTIFACTS_DIR="artifacts"

# Build OSModifier from a local clone of azure-linux-image-tools.
# Make sure the repo has been cloned manually, via:
#
# git clone https://github.com/microsoft/azure-linux-image-tools

artifacts/osmodifier: packaging/docker/Dockerfile-osmodifier.azl3
@docker build -t trident/osmodifier-build:latest \
-f packaging/docker/Dockerfile-osmodifier.azl3 \
.
@mkdir -p "$(ARTIFACTS_DIR)"
@id=$$(docker create trident/osmodifier-build:latest) && \
docker cp -q $$id:/work/azure-linux-image-tools/toolkit/out/tools/osmodifier $@ || \
docker rm -v $$id
@touch $@

.PHONY: azl3-builder-image clean-azl3-builder-image build-azl3
azl3-builder-image:
@echo "Checking for local image $(AZL3_BUILDER_IMAGE)..."
Expand All @@ -185,7 +168,7 @@ target/azl3/release/trident target/azl3/release/trident-acl-agent: version-vars
cargo build --color always --target-dir target/azl3 --release --features dangerous-options,grpc-preview -p trident -p trident-acl-agent

# This will do a proper build on azl3, exactly as the pipelines would, with the custom registry and all.
bin/trident-rpms-azl3.tar.gz: packaging/docker/Dockerfile.full packaging/systemd/*.service packaging/rpm/trident.spec artifacts/osmodifier packaging/selinux-policy-trident/* version-vars
bin/trident-rpms-azl3.tar.gz: packaging/docker/Dockerfile.full packaging/systemd/*.service packaging/rpm/trident.spec packaging/selinux-policy-trident/* version-vars
$(eval CARGO_REGISTRIES_BMP_PUBLICPACKAGES_TOKEN := $(shell az account get-access-token --query "join(' ', ['Bearer', accessToken])" --output tsv))

@mkdir -p bin/
Expand All @@ -207,7 +190,7 @@ bin/trident-rpms-azl3.tar.gz: packaging/docker/Dockerfile.full packaging/systemd
@tar xf $@ -C bin/

# This one does a fast trick-build where we build locally and inject the binary into the container to add it to the RPM.
bin/trident-rpms.tar.gz: packaging/docker/Dockerfile.azl3 packaging/systemd/*.service packaging/rpm/trident.spec artifacts/osmodifier target/release/trident target/release/trident-acl-agent packaging/selinux-policy-trident/*
bin/trident-rpms.tar.gz: packaging/docker/Dockerfile.azl3 packaging/systemd/*.service packaging/rpm/trident.spec target/release/trident target/release/trident-acl-agent packaging/selinux-policy-trident/*
@mkdir -p bin/
@if [ ! -f bin/trident ] || ! cmp -s target/release/trident bin/trident; then \
cp target/release/trident bin/trident; \
Expand Down Expand Up @@ -390,7 +373,7 @@ functional-test: artifacts/trident-functest.qcow2
# A target for pipelines that skips all setup and building steps that are not
# required in the pipeline environment.
.PHONY: functional-test-core
functional-test-core: artifacts/osmodifier build-functional-test-cc generate-functional-test-manifest artifacts/trident-functest.qcow2 bin/virtdeploy
functional-test-core: build-functional-test-cc generate-functional-test-manifest artifacts/trident-functest.qcow2 bin/virtdeploy
python3 -u -m \
pytest --color=yes \
--log-level=INFO \
Expand All @@ -407,7 +390,7 @@ functional-test-core: artifacts/osmodifier build-functional-test-cc generate-fun
--build-output $(BUILD_OUTPUT)

.PHONY: patch-functional-test
patch-functional-test: artifacts/osmodifier build-functional-test-cc generate-functional-test-manifest
patch-functional-test: build-functional-test-cc generate-functional-test-manifest
python3 -u -m \
pytest --color=yes \
--log-level=INFO \
Expand Down Expand Up @@ -566,16 +549,14 @@ RUN_NETLAUNCH_TRIDENT_BIN ?= $(if $(filter yes,$(IS_UBUNTU_24_OR_NEWER)),target/
RUN_NETLAUNCH_LAUNCHER_BIN ?= $(if $(filter yes,$(IS_UBUNTU_24_OR_NEWER)),target/azl3/release/trident-acl-agent,target/release/trident-acl-agent)

.PHONY: run-netlaunch run-netlaunch-stream
run-netlaunch: $(NETLAUNCH_CONFIG) $(TRIDENT_CONFIG) $(NETLAUNCH_ISO) bin/netlaunch validate artifacts/osmodifier $(RUN_NETLAUNCH_TRIDENT_BIN) $(RUN_NETLAUNCH_LAUNCHER_BIN)
run-netlaunch: $(NETLAUNCH_CONFIG) $(TRIDENT_CONFIG) $(NETLAUNCH_ISO) bin/netlaunch validate $(RUN_NETLAUNCH_TRIDENT_BIN) $(RUN_NETLAUNCH_LAUNCHER_BIN)
@echo "Using trident binary: $(RUN_NETLAUNCH_TRIDENT_BIN)"
@mkdir -p artifacts/test-image
@cp $(RUN_NETLAUNCH_TRIDENT_BIN) artifacts/test-image/trident
@cp $(RUN_NETLAUNCH_LAUNCHER_BIN) artifacts/test-image/trident-acl-agent
@cp artifacts/osmodifier artifacts/test-image/
@bin/netlaunch \
--trident-binary $(RUN_NETLAUNCH_TRIDENT_BIN) \
--launcher-binary $(RUN_NETLAUNCH_LAUNCHER_BIN) \
--osmodifier-binary artifacts/osmodifier \
--rcp-agent-mode cli \
--iso $(NETLAUNCH_ISO) \
$(if $(NETLAUNCH_PORT),--port $(NETLAUNCH_PORT)) \
Expand All @@ -587,15 +568,13 @@ run-netlaunch: $(NETLAUNCH_CONFIG) $(TRIDENT_CONFIG) $(NETLAUNCH_ISO) bin/netlau
--trace-file trident-metrics.jsonl \
$(if $(LOG_TRACE),--log-trace)

run-netlaunch-stream: $(NETLAUNCH_CONFIG) $(TRIDENT_CONFIG) $(NETLAUNCH_ISO) bin/netlaunch artifacts/osmodifier $(RUN_NETLAUNCH_TRIDENT_BIN)
run-netlaunch-stream: $(NETLAUNCH_CONFIG) $(TRIDENT_CONFIG) $(NETLAUNCH_ISO) bin/netlaunch $(RUN_NETLAUNCH_TRIDENT_BIN)
@echo "Using trident binary: $(RUN_NETLAUNCH_TRIDENT_BIN)"
@mkdir -p artifacts/test-image
@cp $(RUN_NETLAUNCH_TRIDENT_BIN) artifacts/test-image/trident
@cp artifacts/osmodifier artifacts/test-image/
@bin/netlaunch \
--stream-image \
--trident-binary $(RUN_NETLAUNCH_TRIDENT_BIN) \
--osmodifier-binary artifacts/osmodifier \
--rcp-agent-mode cli \
--iso $(NETLAUNCH_ISO) \
$(if $(NETLAUNCH_PORT),--port $(NETLAUNCH_PORT)) \
Expand Down
27 changes: 27 additions & 0 deletions crates/osmodifier/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[package]
name = "osmodifier"
version = "0.1.0"
edition = "2021"
publish = false
license = "MIT"
description = "OS modifier library - applies OS configuration changes (users, hostname, services, modules, boot config, SELinux)"

[dependencies]
anyhow = { workspace = true }
inventory = { workspace = true }
log = { workspace = true }
regex = { workspace = true }
serde = { workspace = true }
serde_yaml = { workspace = true }
tempfile = { workspace = true }

pytest = { path = "../pytest" }
pytest_gen = { path = "../pytest_gen" }
trident_api = { path = "../trident_api" }
osutils = { path = "../osutils" }

[dev-dependencies]
indoc = { workspace = true }

[features]
functional-test = []
152 changes: 152 additions & 0 deletions crates/osmodifier/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# osmodifier

Native Rust port of the OS modifier functionality from
[azure-linux-image-tools](https://github.com/microsoft/azure-linux-image-tools).

Trident calls osmodifier functions directly as a library crate instead of
serializing config to YAML, writing a temp file, and exec'ing the Go binary.

## Port Origin

The initial port was made on **2026-05-11** (commit `ba55580`) from the
azure-linux-image-tools repository. The Go code spans three packages under
`toolkit/tools/`:

| Go package | Purpose |
|------------|---------|
| `osmodifier/` | CLI entry point |
| `osmodifierapi/` | Configuration types and validation |
| `pkg/osmodifierlib/` | Core modification logic |
| `pkg/imagecustomizerlib/` | Shared helpers (users, hostname, services, modules) |

## File Mapping

Each Rust source file and the Go file(s) it was ported from:

| Rust file | Go source(s) | Go commit | Date |
|-----------|--------------|-----------|------|
| `lib.rs` | `pkg/osmodifierlib/osmodifier.go`, `pkg/osmodifierlib/modifierutils.go` | `f4de1a0` | 2026-03-17 |
| `config.rs` | `osmodifierapi/os.go`, `osmodifierapi/overlay.go`, `osmodifierapi/verity.go`, `osmodifierapi/identifiedpartition.go` | `8bd4ef3` | 2025-09-02 |
| `users.rs` | `pkg/imagecustomizerlib/customizeusers.go` | `8bd4ef3` | 2025-09-02 |
| `hostname.rs` | `pkg/imagecustomizerlib/customizehostname.go` | `8bd4ef3` | 2025-09-02 |
| `modules.rs` | `pkg/imagecustomizerlib/kernelmoduleutils.go` | `8bd4ef3` | 2025-09-02 |
| `services.rs` | `pkg/imagecustomizerlib/customizeservices.go` | `dc90945` | 2026-03-31 |
| `selinux.rs` | `pkg/osmodifierlib/modifierutils.go` (SELinux functions) | `f4de1a0` | 2026-03-17 |
| `default_grub.rs` | `pkg/osmodifierlib/modifydefaultgrub.go` | `f4de1a0` | 2026-03-17 |
| `grub_cfg.rs` | `pkg/osmodifierlib/modifydefaultgrub.go`, `pkg/osmodifierlib/modifierutils.go` | `f4de1a0` | 2026-03-17 |

All Go paths are relative to `toolkit/tools/` in the azure-linux-image-tools
repository. The Go commit column is the latest commit touching that file at the
time of the port.

## Key Differences from the Go Implementation

### Library instead of binary

The Go osmodifier is a standalone CLI binary invoked via `exec`. The Rust
version is a library crate exposing three public functions:

```rust
osmodifier::modify_os(&ctx, &config)?; // replaces: osmodifier --config-file
osmodifier::modify_boot(&ctx, &boot_config)?; // replaces: osmodifier --config-file (boot subset)
osmodifier::update_default_grub(&ctx)?; // replaces: osmodifier --update-grub
```

**Reasoning:** Eliminates YAML serialization round-trips, temp file I/O, and
process spawning overhead. Errors propagate as native Rust `Result` types
instead of being parsed from stderr.

### No chroot / safechroot

The Go code uses `safechroot` to enter a chroot environment before making
modifications. The Rust version assumes it is already running inside the
chroot (trident manages the chroot lifecycle at a higher level). File
operations use `OsModifierContext` for path resolution; system tool
invocations (`useradd`, `usermod`, etc.) run directly against `/`.

**Reasoning:** Trident always chroots into newroot before calling osmodifier.
Duplicating chroot enter/exit here would conflict with the outer chroot
management and add unnecessary complexity.

### Inlined imagecustomizerlib logic

The Go osmodifier delegates user, hostname, service, and module management to
`imagecustomizerlib`, a shared library also used by the image customizer tool.
The Rust port inlines this logic into dedicated modules (`users.rs`,
`hostname.rs`, `services.rs`, `modules.rs`).

**Reasoning:** Trident only needs the osmodifier subset of imagecustomizerlib.
Porting the full shared library would pull in unnecessary dependencies. Inlining
keeps the crate self-contained and avoids coupling to Go-side refactors in the
shared library.

### Secure password handling

The Go code sets passwords via `useradd -p <hash>`, which exposes the password
hash in `/proc/<pid>/cmdline`. The Rust version uses `chpasswd -e` with the
hash passed via stdin.

**Reasoning:** Defense in depth. Any process on the system can read
`/proc/cmdline`, making the hash visible during user creation. Passing it via
stdin keeps the hash out of the process argument list.

### Atomic file writes

The Rust code uses `tempfile::NamedTempFile::persist()` for all writes to
sensitive files (`/etc/shadow`, `/etc/passwd`). The Go code writes directly.

**Reasoning:** Atomic rename prevents partial writes from corrupting critical
auth files if the process is interrupted mid-write.

### Startup command validation

The Rust code validates that startup commands do not contain colons or newlines
before writing to `/etc/passwd`. The Go code does not perform this validation.

**Reasoning:** `/etc/passwd` is colon-delimited and newline-separated. A
malicious or malformed startup command containing these characters could corrupt
the passwd file or inject additional entries.

### Split boot configuration API

The Go binary handles OS and boot modifications in a single `--config-file`
invocation. The Rust version splits this into `modify_os()` and `modify_boot()`
with separate config types (`OSModifierConfig` and `BootConfig`).

**Reasoning:** OS modifications (users, hostname, services) and boot
modifications (SELinux, overlays, verity) happen at different stages of the
Trident image build pipeline. Separating them avoids passing unused
configuration and makes the call sites clearer.

### System tool access via Dependency enum

External tool invocations use the trident `osutils::Dependency` enum instead
of calling `std::process::Command` directly. This provides consistent binary
resolution (via `which`), structured error reporting, and a centralized
inventory of runtime dependencies.

| Dependency variant | Used in |
|--------------------|---------|
| `Systemctl` | `services.rs` — enable/disable services |
| `Grub2Mkconfig` | `grub_cfg.rs` — regenerate GRUB config |
| `Id` | `users.rs` — check if a user exists |
| `Useradd` | `users.rs` — create new users |
| `Usermod` | `users.rs` — modify groups |
| `Chown` | `users.rs` — set file ownership |

Two tools still use `std::process::Command` directly because the Dependency
`Command` wrapper does not yet support stdin piping:

- **`openssl passwd`** (`hash_password`) — reads plaintext from stdin
- **`chpasswd -e`** (`set_password_via_chpasswd`) — reads `user:hash` from stdin

## Keeping the Port in Sync

When the Go osmodifier code changes upstream, compare the diff against the
corresponding Rust module using the file mapping table above. Pay special
attention to:

- New fields added to config structs in `osmodifierapi/`
- New modification steps in `modifierutils.go`
- Changes to GRUB parsing logic in `modifydefaultgrub.go`
- Changes to user/service/module handling in `imagecustomizerlib/`
Loading
Loading