It's true. Do you think I should revert? I sanitize the type of file that
we get in?

Le lun. 1 juin 2026 à 12:25, Jean-Baptiste Kempf via ffmpeg-devel <
[email protected]> a écrit :

> This is particularly dangerous.
> This makes the CI prone to injection to files from random people.
>
> On Mon, 1 Jun 2026, at 17:41, Romain Beauxis via ffmpeg-cvslog wrote:
> > This is an automated email from the git hooks/post-receive script.
> >
> > Git pushed a commit to branch master
> > in repository ffmpeg.
> >
> > commit 78fff004f021fc9b5a3467317eaab7deb446c955
> > Author:     Romain Beauxis <[email protected]>
> > AuthorDate: Wed May 27 08:09:12 2026 -0500
> > Commit:     Romain Beauxis <[email protected]>
> > CommitDate: Mon Jun 1 10:40:57 2026 -0500
> >
> >     .forgejo: add support for ephemeral FATE samples via PR attachments
> >
> >     Developers can attach sample files to a PR and list their target
> paths
> >     within the fate-suite in a fate-samples block in the PR description:
> >
> >       ```fate-samples
> >       vorbis/tos.ogg
> >       mov/some-new-sample.mov
> >       ```
> >
> >     A new inject-pr-samples.py script fetches the PR metadata from the
> >     Forgejo API, resolves each listed path to its matching attachment by
> >     filename, and downloads the files into the fate-suite directory
> before
> >     FATE runs.
> >
> >     The script validates that pr-number is an integer, that paths are
> >     relative, contain no '..', and are at most 3 components deep
> (matching
> >     the deepest paths in the existing fate-suite).  Attachment URLs are
> >     restricted to the code.ffmpeg.org domain.
> >
> >     The script exports a new_samples=true/false output via
> $FORGEJO_OUTPUT.
> >     After FATE completes, a final workflow step fails the run if any new
> >     sample was injected, reminding contributors to add their samples to
> the
> >     official fate-suite before the PR can be merged.
> >
> >     The script can also be used locally:
> >       SAMPLES=/path/to/fate-suite .forgejo/inject-pr-samples.py
> <pr-number>
> > ---
> >  .forgejo/inject-pr-samples.py | 174
> ++++++++++++++++++++++++++++++++++++++++++
> >  .forgejo/workflows/test.yml   |  18 +++++
> >  2 files changed, 192 insertions(+)
> >
> > diff --git a/.forgejo/inject-pr-samples.py
> > b/.forgejo/inject-pr-samples.py
> > new file mode 100755
> > index 0000000000..3f50067751
> > --- /dev/null
> > +++ b/.forgejo/inject-pr-samples.py
> > @@ -0,0 +1,174 @@
> > +#!/usr/bin/env python3
> > +# Copyright (c) 2026 Romain Beauxis <[email protected]>
> > +#
> > +# Redistribution and use in source and binary forms, with or without
> > +# modification, are permitted provided that the following conditions
> > are met:
> > +#
> > +# 1. Redistributions of source code must retain the above copyright
> > notice,
> > +#    this list of conditions and the following disclaimer.
> > +# 2. Redistributions in binary form must reproduce the above copyright
> > notice,
> > +#    this list of conditions and the following disclaimer in the
> > documentation
> > +#    and/or other materials provided with the distribution.
> > +#
> > +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
> > "AS IS"
> > +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
> > TO, THE
> > +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
> > PURPOSE
> > +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
> > CONTRIBUTORS BE
> > +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
> > +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
> > +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
> > BUSINESS
> > +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
> > IN
> > +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
> > OTHERWISE)
> > +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
> > OF THE
> > +# POSSIBILITY OF SUCH DAMAGE.
> > +
> > +"""Inject PR attachment samples into the fate-suite directory.
> > +
> > +Usage: inject-pr-samples.py <pr-number>
> > +
> > +Reads SAMPLES from the environment (defaults to fate-suite).  For each
> > path
> > +listed in a ```fate-samples``` block in the PR description, downloads
> > the
> > +matching PR attachment into $SAMPLES/<path>.
> > +
> > +The PR description should contain a block like:
> > +
> > +  ```fate-samples
> > +  vorbis/tos.ogg
> > +  mov/some-new-sample.mov
> > +  ```
> > +
> > +Each filename must match a file attached to the PR.
> > +"""
> > +
> > +import hashlib
> > +import json
> > +import os
> > +import re
> > +import sys
> > +import tempfile
> > +import urllib.request
> > +from pathlib import Path, PurePosixPath
> > +
> > +FORGEJO_API =
> > "https://code.ffmpeg.org/api/v1/repos/ffmpeg/ffmpeg/issues";
> > +ATTACHMENT_BASE = "https://code.ffmpeg.org/attachments/";
> > +
> > +
> > +def fetch_json(url):
> > +    with urllib.request.urlopen(url) as r:
> > +        return json.load(r)
> > +
> > +
> > +def parse_fate_samples(body):
> > +    paths = []
> > +    in_block = False
> > +    for line in body.splitlines():
> > +        if line == "```fate-samples":
> > +            in_block = True
> > +        elif line == "```" and in_block:
> > +            break
> > +        elif in_block:
> > +            parts = line.split()
> > +            if len(parts) == 1:
> > +                paths.append(parts[0])
> > +    return paths
> > +
> > +
> > +MAX_PATH_DEPTH = 3
> > +
> > +
> > +def validate_path(path):
> > +    p = PurePosixPath(path)
> > +    if p.is_absolute():
> > +        raise ValueError(f"path must be relative: {path!r}")
> > +    if ".." in p.parts:
> > +        raise ValueError(f"path must not contain '..': {path!r}")
> > +    if not p.parts:
> > +        raise ValueError(f"empty path")
> > +    if len(p.parts) > MAX_PATH_DEPTH:
> > +        raise ValueError(f"path too deep (max {MAX_PATH_DEPTH}
> > components): {path!r}")
> > +
> > +
> > +def validate_url(url):
> > +    if not url.startswith(ATTACHMENT_BASE):
> > +        raise ValueError(f"unexpected attachment URL: {url!r}")
> > +
> > +
> > +def digest(path):
> > +    h = hashlib.sha256()
> > +    with open(path, "rb") as f:
> > +        while chunk := f.read(1 << 16):
> > +            h.update(chunk)
> > +    return h.digest()
> > +
> > +
> > +def download(url, dst):
> > +    dst.parent.mkdir(parents=True, exist_ok=True)
> > +    with tempfile.NamedTemporaryFile(dir=dst.parent, delete=False) as
> > tmp:
> > +        tmp_path = Path(tmp.name)
> > +        try:
> > +            with urllib.request.urlopen(url) as r:
> > +                while chunk := r.read(1 << 16):
> > +                    tmp.write(chunk)
> > +            if dst.exists() and digest(dst) != digest(tmp_path):
> > +                raise ValueError(f"already exists with different
> > content: {dst}")
> > +            tmp_path.rename(dst)
> > +        except:
> > +            tmp_path.unlink(missing_ok=True)
> > +            raise
> > +
> > +
> > +def main():
> > +    if len(sys.argv) != 2 or not re.fullmatch(r"[0-9]+", sys.argv[1]):
> > +        print(f"Usage: {sys.argv[0]} <pr-number>", file=sys.stderr)
> > +        sys.exit(1)
> > +
> > +    pr_number = sys.argv[1]
> > +    samples_dir = Path(os.environ.get("SAMPLES", "fate-suite"))
> > +
> > +    pr = fetch_json(f"{FORGEJO_API}/{pr_number}")
> > +    assets = {a["name"]: a["browser_download_url"] for a in
> > pr.get("assets", [])}
> > +    paths = parse_fate_samples(pr.get("body", ""))
> > +
> > +    if not paths:
> > +        sys.exit(0)
> > +
> > +    new_samples = False
> > +
> > +    for path in paths:
> > +        try:
> > +            validate_path(path)
> > +        except ValueError as e:
> > +            print(f"fate-samples: {e}", file=sys.stderr)
> > +            sys.exit(1)
> > +
> > +        name = PurePosixPath(path).name
> > +        url = assets.get(name)
> > +        if url is None:
> > +            print(f"fate-samples: no attachment named {name!r}",
> > file=sys.stderr)
> > +            sys.exit(1)
> > +
> > +        try:
> > +            validate_url(url)
> > +        except ValueError as e:
> > +            print(f"fate-samples: {e}", file=sys.stderr)
> > +            sys.exit(1)
> > +
> > +        dst = samples_dir / path
> > +        is_new = not dst.exists()
> > +        try:
> > +            download(url, dst)
> > +        except ValueError as e:
> > +            print(f"fate-samples: {e}", file=sys.stderr)
> > +            sys.exit(1)
> > +        if is_new:
> > +            new_samples = True
> > +        print(f"Injected: {path}")
> > +
> > +    output_file = os.environ.get("FORGEJO_OUTPUT")
> > +    if output_file:
> > +        with open(output_file, "a") as f:
> > +            print(f"new_samples={'true' if new_samples else 'false'}",
> > file=f)
> > +
> > +
> > +if __name__ == "__main__":
> > +    main()
> > diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml
> > index 342120188e..3af1522b88 100644
> > --- a/.forgejo/workflows/test.yml
> > +++ b/.forgejo/workflows/test.yml
> > @@ -58,11 +58,20 @@ jobs:
> >          with:
> >            path: fate-suite
> >            key: fate-suite-${{ steps.fate.outputs.hash }}
> > +      - name: Inject PR Samples
> > +        id: inject
> > +        if: ${{ forge.event_name == 'pull_request' }}
> > +        run: SAMPLES=$PWD/fate-suite .forgejo/inject-pr-samples.py ${{
> > forge.event.pull_request.number }}
> >        - name: Run Fate
> >          run: |
> >            LD_LIBRARY_PATH="$(printf "%s:" "$PWD"/lib*)$PWD" make fate
> > fate-build SAMPLES="$PWD/fate-suite" -j$(nproc) || FATERES=$?
> >            find . -name "*.err" -exec printf '::group::%s\n' {} \;
> > -exec cat {} \; -exec printf '::endgroup::\n' \;
> >            exit ${FATERES:-0}
> > +      - name: Fail if new samples were injected
> > +        if: ${{ steps.inject.outputs.new_samples == 'true' }}
> > +        run: |
> > +          echo "New FATE samples were injected from PR attachments.
> > Please add them to the official fate-suite before merging."
> > +          exit 1
> >    run_fate_full:
> >      name: Fate (Full, ${{ matrix.target_exec }})
> >      strategy:
> > @@ -110,6 +119,10 @@ jobs:
> >          with:
> >            path: fate-suite
> >            key: fate-suite-${{ steps.fate.outputs.hash }}
> > +      - name: Inject PR Samples
> > +        id: inject
> > +        if: ${{ forge.event_name == 'pull_request' }}
> > +        run: SAMPLES=$PWD/fate-suite
> > ffmpeg/.forgejo/inject-pr-samples.py ${{
> > forge.event.pull_request.number }}
> >        - name: Run Fate
> >          run: |
> >            if [[ "${{ matrix.target_exec }}" == "wine" ]]; then
> > @@ -119,3 +132,8 @@ jobs:
> >            LD_LIBRARY_PATH="$(printf "%s:" "$PWD"/lib*)$PWD" make -C
> > build fate fate-build SAMPLES="$PWD/fate-suite" -j$(nproc) || FATERES=$?
> >            find . -name "*.err" -exec printf '::group::%s\n' {} \;
> > -exec cat {} \; -exec printf '::endgroup::\n' \;
> >            exit ${FATERES:-0}
> > +      - name: Fail if new samples were injected
> > +        if: ${{ steps.inject.outputs.new_samples == 'true' }}
> > +        run: |
> > +          echo "New FATE samples were injected from PR attachments.
> > Please add them to the official fate-suite before merging."
> > +          exit 1
> >
> > _______________________________________________
> > ffmpeg-cvslog mailing list -- [email protected]
> > To unsubscribe send an email to [email protected]
>
> --
> Jean-Baptiste Kempf -  President
> +33 672 704 734
> https://jbkempf.com/
> _______________________________________________
> ffmpeg-devel mailing list -- [email protected]
> To unsubscribe send an email to [email protected]
>
_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to