Skip to content

Commit ac6f6e2

Browse files
authored
bugfix: repro, #988 (#989)
1 parent b98dcb1 commit ac6f6e2

File tree

9 files changed

+93
-10
lines changed

9 files changed

+93
-10
lines changed

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ indicatif = "0.17.11"
4242
insta = "1.43.0"
4343
jsonschema = "0.30.0"
4444
line-index = "0.1.2"
45+
memchr = "2.7.5"
4546
owo-colors = "4.2.1"
4647
regex = "1.11.1"
4748
reqwest = { version = "0.12.20", default-features = false }

crates/github-actions-expressions/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ impl<'src> Literal<'src> {
134134
// TODO: Move this type to some kind of common crate? It's useful to have
135135
// a single span type everywhere, instead of the current mash of span helpers
136136
// and ranges we have throughout zizmor.
137-
/// Represents a `[start, end)` span for a source expression.
137+
/// Represents a `[start, end)` byte span for a source expression.
138138
#[derive(Copy, Clone, Debug, PartialEq)]
139139
pub struct Span {
140140
/// The start of the span, inclusive.

crates/zizmor/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ indicatif.workspace = true
4747
itertools.workspace = true
4848
jsonschema.workspace = true
4949
line-index.workspace = true
50+
memchr.workspace = true
5051
owo-colors.workspace = true
5152
regex.workspace = true
5253
reqwest = { workspace = true, features = ["blocking", "json", "rustls-tls"] }

crates/zizmor/src/finding/location.rs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,11 @@ pub(crate) enum Fragment<'doc> {
129129
/// might contain multiple lines, e.g. a multi-line GitHub Actions
130130
/// expression, since the subfeature's indentation won't necessarily match
131131
/// the surrounding feature's YAML-level indentation.
132-
Regex(#[serde(serialize_with = "Fragment::serialize_regex")] regex::Regex),
132+
Regex(#[serde(serialize_with = "Fragment::serialize_regex")] regex::bytes::Regex),
133133
}
134134

135135
impl<'doc> Fragment<'doc> {
136-
fn serialize_regex<S>(regex: &regex::Regex, serializer: S) -> Result<S::Ok, S::Error>
136+
fn serialize_regex<S>(regex: &regex::bytes::Regex, serializer: S) -> Result<S::Ok, S::Error>
137137
where
138138
S: serde::Serializer,
139139
{
@@ -172,7 +172,7 @@ impl<'doc> Fragment<'doc> {
172172
static WHITESPACE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\s+").unwrap());
173173
let regex = WHITESPACE.replace_all(&escaped, "\\s+");
174174

175-
Fragment::Regex(Regex::new(&regex).unwrap())
175+
Fragment::Regex(regex::bytes::Regex::new(&regex).unwrap())
176176
}
177177
}
178178
}
@@ -211,14 +211,26 @@ impl<'doc> Subfeature<'doc> {
211211
/// can't be found. The returned span is relative to the feature's
212212
/// start.
213213
fn locate_within(&self, feature: &str) -> Option<Span> {
214+
// NOTE: Our inputs are always valid UTF-8 but `after` may not
215+
// be a valid UTF-8 codepoint index, so everything below operates
216+
// on a byte slice.
217+
// Why, you might ask, might `after` not be a valid codepoint index?
218+
// Because `after` is a fuzzy anchor: we know our subfeature starts
219+
// *somewhere* after `after`, but we don't know exactly where.
220+
// This happens because we have a rough sense of where the subfeature
221+
// is *after* YAML parsing, but we don't know exactly where it is
222+
// in the original YAML feature due to significant whitespace.
223+
let feature = feature.as_bytes();
214224
let bias = self.after;
215225
let focus = &feature[bias..];
216226

217227
match &self.fragment {
218-
Fragment::Raw(fragment) => focus.find(fragment).map(|start| {
219-
let end = start + fragment.len();
220-
Span::from(start..end).adjust(bias)
221-
}),
228+
Fragment::Raw(fragment) => {
229+
memchr::memmem::find(focus, fragment.as_bytes()).map(|start| {
230+
let end = start + fragment.len();
231+
Span::from(start..end).adjust(bias)
232+
})
233+
}
222234
Fragment::Regex(regex) => regex
223235
.find(focus)
224236
.map(|m| Span::from(m.range()).adjust(bias)),

crates/zizmor/tests/integration/snapshot.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,13 @@ fn template_injection() -> Result<()> {
413413
.run()?
414414
);
415415

416+
insta::assert_snapshot!(
417+
zizmor()
418+
.input(input_under_test("template-injection/issue-988-repro.yml"))
419+
.args(["--persona=pedantic"])
420+
.run()?
421+
);
422+
416423
Ok(())
417424
}
418425

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
source: crates/zizmor/tests/integration/snapshot.rs
3+
expression: "zizmor().input(input_under_test(\"template-injection/issue-988-repro.yml\")).args([\"--persona=pedantic\"]).run()?"
4+
snapshot_kind: text
5+
---
6+
note[template-injection]: code injection via template expansion
7+
--> @@INPUT@@:16:29
8+
|
9+
13 | run: |
10+
| --- note: this run block
11+
14 | for index in {1..2}; do
12+
15 | # ドドド
13+
16 | event_name="${{ github.event_name }}"
14+
| ----------------- note: may expand into attacker-controllable code
15+
|
16+
= note: audit confidenceUnknown
17+
18+
note[template-injection]: code injection via template expansion
19+
--> @@INPUT@@:27:57
20+
|
21+
25 | run: |
22+
| --- note: this run block
23+
26 | curl -X POST https://api.example.com -H "Content-type: application/json" \
24+
27 | -d "{\"text\":\"ドドド: https://github.com/${{ github.repository }}\"}"
25+
| ----------------- note: may expand into attacker-controllable code
26+
|
27+
= note: audit confidenceUnknown
28+
29+
2 findings: 2 unknown, 0 informational, 0 low, 0 medium, 0 high
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# https://github.com/zizmorcore/zizmor/issues/988
2+
name: Panic
3+
on:
4+
pull_request: {}
5+
permissions: {}
6+
jobs:
7+
example1:
8+
name: "Example 1"
9+
runs-on: ubuntu-latest
10+
timeout-minutes: 5
11+
steps:
12+
- name: "Panic occurred in file 'crates/zizmor/src/finding/location.rs' at line 215"
13+
run: |
14+
for index in {1..2}; do
15+
# ドドド
16+
event_name="${{ github.event_name }}"
17+
echo "$index: $event_name"
18+
done
19+
example2:
20+
name: "Example 2"
21+
runs-on: ubuntu-latest
22+
timeout-minutes: 5
23+
steps:
24+
- name: "Panic occurred in file 'crates/zizmor/src/finding/location.rs' at line 215"
25+
run: |
26+
curl -X POST https://api.example.com -H "Content-type: application/json" \
27+
-d "{\"text\":\"ドドド: https://github.com/${{ github.repository }}\"}"

docs/release-notes.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ of `zizmor`.
1515
* The [bot-conditions] audit now produces findings on triggers other than
1616
`pull_request_target` (#921)
1717

18+
### Bug Fixes 🐛
19+
20+
* Fixed a bug where `zizmor` would crash when attempting to extract
21+
subfeatures from features containing non-ASCII codepoints (#989)
22+
1823
## 1.10.0
1924

2025
This is a **huge** new release, with multiple new features, enhancements,

0 commit comments

Comments
 (0)