[chore] ffmpeg webassembly fiddling (#4454)

This disables ffmpeg / ffprobe support on platforms where the wazero compiler is not available. The slowness introduced is hard to pindown for admins (and us!), so it's easier to just return an error message linking to docs on attempted media processing. It still allows the instance to run, just erroring if anything other than a jpeg is attempted to be processed. This should hopefully make it easier for users to notice these issues.

Also further locks down our wazero 'allowFiles' fs and other media code to address: https://codeberg.org/superseriousbusiness/gotosocial/issues/4408

relates to: https://codeberg.org/superseriousbusiness/gotosocial/issues/4427
also relates to issues raised in #gotosocial-help on matrix

closes https://codeberg.org/superseriousbusiness/gotosocial/issues/4408

Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4454
Co-authored-by: kim <grufwub@gmail.com>
Co-committed-by: kim <grufwub@gmail.com>
This commit is contained in:
kim 2025-09-24 15:12:25 +02:00 committed by kim
commit 3db2d42247
7 changed files with 111 additions and 97 deletions

View file

@ -328,7 +328,7 @@ This is the current status of support offered by GoToSocial for different platfo
Notes on 64-bit CPU feature requirements:
- x86_64 requires the SSE4.1 instruction set. (CPUs manufactured after ~2010)
- x86_64 requires the [x86-64-v2](https://en.wikipedia.org/wiki/X86-64-v2) level instruction sets. (CPUs manufactured after ~2010)
- ARM64 requires no specific features, ARMv8 CPUs (and later) have all required features.

View file

@ -21,8 +21,6 @@ import (
"context"
"encoding/json"
"errors"
"os"
"path"
"strconv"
"strings"
@ -158,34 +156,20 @@ func ffmpeg(ctx context.Context, inpath string, outpath string, args ...string)
Config: func(modcfg wazero.ModuleConfig) wazero.ModuleConfig {
fscfg := wazero.NewFSConfig()
// Needs read-only access to
// /dev/urandom for some types.
urandom := &allowFiles{
{
abs: "/dev/urandom",
flag: os.O_RDONLY,
perm: 0,
},
}
fscfg = fscfg.WithFSMount(urandom, "/dev")
// Needs read-only access /dev/urandom,
// required by some ffmpeg operations.
fscfg = fscfg.WithFSMount(&allowFiles{
allowRead("/dev/urandom"),
}, "/dev")
// In+out dirs are always the same (tmp),
// so we can share one file system for
// both + grant different perms to inpath
// (read only) and outpath (read+write).
shared := &allowFiles{
{
abs: inpath,
flag: os.O_RDONLY,
perm: 0,
},
{
abs: outpath,
flag: os.O_RDWR | os.O_CREATE | os.O_TRUNC,
perm: 0666,
},
}
fscfg = fscfg.WithFSMount(shared, path.Dir(inpath))
fscfg = fscfg.WithFSMount(&allowFiles{
allowCreate(outpath),
allowRead(inpath),
}, tmpdir)
// Set anonymous module name.
modcfg = modcfg.WithName("")
@ -246,16 +230,10 @@ func ffprobe(ctx context.Context, filepath string) (*result, error) {
Config: func(modcfg wazero.ModuleConfig) wazero.ModuleConfig {
fscfg := wazero.NewFSConfig()
// Needs read-only access
// to file being probed.
in := &allowFiles{
{
abs: filepath,
flag: os.O_RDONLY,
perm: 0,
},
}
fscfg = fscfg.WithFSMount(in, path.Dir(filepath))
// Needs read-only access to probed file.
fscfg = fscfg.WithFSMount(&allowFiles{
allowRead(filepath),
}, tmpdir)
// Set anonymous module name.
modcfg = modcfg.WithName("")

View file

@ -21,12 +21,12 @@ package ffmpeg
import (
"context"
"errors"
"os"
"runtime"
"sync/atomic"
"unsafe"
"code.superseriousbusiness.org/gotosocial/internal/log"
"codeberg.org/gruf/go-ffmpreg/embed"
"codeberg.org/gruf/go-ffmpreg/wasm"
"github.com/tetratelabs/wazero"
@ -49,24 +49,19 @@ func initWASM(ctx context.Context) error {
return nil
}
var cfg wazero.RuntimeConfig
// Allocate new runtime config, letting
// wazero determine compiler / interpreter.
cfg = wazero.NewRuntimeConfig()
// Though still perform a check of CPU features at
// runtime to warn about slow interpreter performance.
if reason, supported := compilerSupported(); !supported {
log.Warn(ctx, "!!! WAZERO COMPILER MAY NOT BE AVAILABLE !!!"+
" Reason: "+reason+"."+
" Wazero will likely fall back to interpreter mode,"+
" resulting in poor performance for media processing (and SQLite, if in use)."+
" For more info and possible workarounds, please check:"+
" https://docs.gotosocial.org/en/latest/getting_started/releases/#supported-platforms",
)
// Check at runtime whether Wazero compiler support is available,
// interpreter mode is too slow for a usable gotosocial experience.
if reason, supported := isCompilerSupported(); !supported {
return errors.New("!!! WAZERO COMPILER SUPPORT NOT AVAILABLE !!!" +
" Reason: " + reason + "." +
" Wazero in interpreter mode is too slow to use ffmpeg" +
" (this will also affect SQLite if in use)." +
" For more info and possible workarounds, please check: https://docs.gotosocial.org/en/latest/getting_started/releases/#supported-platforms")
}
// Allocate new runtime compiler config.
cfg := wazero.NewRuntimeConfigCompiler()
if dir := os.Getenv("GTS_WAZERO_COMPILATION_CACHE"); dir != "" {
// Use on-filesystem compilation cache given by env.
cache, err := wazero.NewCompilationCacheWithDir(dir)
@ -128,7 +123,7 @@ func initWASM(ctx context.Context) error {
return nil
}
func compilerSupported() (string, bool) {
func isCompilerSupported() (string, bool) {
switch runtime.GOOS {
case "linux", "android",
"windows", "darwin",
@ -141,10 +136,11 @@ func compilerSupported() (string, bool) {
switch runtime.GOARCH {
case "amd64":
// NOTE: wazero in the future may decouple the
// requirement of simd (sse4_1) from requirements
// requirement of simd (sse4_1+2) from requirements
// for compiler support in the future, but even
// still our module go-ffmpreg makes use of them.
return "amd64 SSE4.1 required", cpu.X86.HasSSE41
return "amd64 x86-64-v2 required (see: https://en.wikipedia.org/wiki/X86-64-v2)",
cpu.Initialized && cpu.X86.HasSSE3 && cpu.X86.HasSSE41 && cpu.X86.HasSSE42
case "arm64":
// NOTE: this particular check may change if we
// later update go-ffmpreg to a version that makes

View file

@ -74,20 +74,28 @@ func clearMetadata(ctx context.Context, filepath string) error {
// terminateExif cleans exif data from file at input path, into file
// at output path, using given file extension to determine cleaning type.
func terminateExif(outpath, inpath string, ext string) error {
func terminateExif(outpath, inpath string, ext string) (err error) {
var inFile *os.File
var outFile *os.File
// Ensure handles
// closed on return.
defer func() {
outFile.Close()
inFile.Close()
}()
// Open input file at given path.
inFile, err := os.Open(inpath)
inFile, err = openRead(inpath)
if err != nil {
return gtserror.Newf("error opening input file %s: %w", inpath, err)
}
defer inFile.Close()
// Open output file at given path.
outFile, err := os.Create(outpath)
// Create output file at given path.
outFile, err = openWrite(outpath)
if err != nil {
return gtserror.Newf("error opening output file %s: %w", outpath, err)
}
defer outFile.Close()
// Terminate EXIF data from 'inFile' -> 'outFile'.
err = terminator.TerminateInto(outFile, inFile, ext)

View file

@ -38,8 +38,9 @@ const (
// probe will first attempt to probe the file at path using native Go code
// (for performance), but falls back to using ffprobe to retrieve media details.
func probe(ctx context.Context, filepath string) (*result, error) {
// Open input file at given path.
file, err := os.Open(filepath)
file, err := openRead(filepath)
if err != nil {
return nil, gtserror.Newf("error opening file %s: %w", filepath, err)
}
@ -80,6 +81,7 @@ func probe(ctx context.Context, filepath string) (*result, error) {
// probeJPEG decodes the given file as JPEG and determines
// image details from the decoded JPEG using native Go code.
func probeJPEG(file *os.File) (*result, error) {
// Attempt to decode JPEG, adding back hdr magic.
cfg, err := jpeg.DecodeConfig(io.MultiReader(
strings.NewReader(magicJPEG),

View file

@ -24,7 +24,6 @@ import (
"image/jpeg"
"image/png"
"io"
"os"
"strings"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
@ -89,8 +88,8 @@ func generateThumb(
// Default type is webp.
mimeType = "image/webp"
// Generate thumb output path REPLACING extension.
if i := strings.IndexByte(filepath, '.'); i != -1 {
// Generate thumb output path REPLACING file extension.
if i := strings.LastIndexByte(filepath, '.'); i != -1 {
outpath = filepath[:i] + "_thumb.webp"
ext = filepath[i+1:] // old extension
} else {
@ -231,7 +230,7 @@ func generateNativeThumb(
error,
) {
// Open input file at given path.
infile, err := os.Open(inpath)
infile, err := openRead(inpath)
if err != nil {
return "", gtserror.Newf("error opening input file %s: %w", inpath, err)
}
@ -272,7 +271,7 @@ func generateNativeThumb(
)
// Open output file at given path.
outfile, err := os.Create(outpath)
outfile, err := openWrite(outpath)
if err != nil {
return "", gtserror.Newf("error opening output file %s: %w", outpath, err)
}
@ -313,8 +312,9 @@ func generateNativeThumb(
// generateWebpBlurhash generates a blurhash for Webp at filepath.
func generateWebpBlurhash(filepath string) (string, error) {
// Open the file at given path.
file, err := os.Open(filepath)
file, err := openRead(filepath)
if err != nil {
return "", gtserror.Newf("error opening input file %s: %w", filepath, err)
}

View file

@ -30,14 +30,41 @@ import (
"codeberg.org/gruf/go-iotools"
)
// media processing tmpdir.
var tmpdir = os.TempDir()
// file represents one file
// with the given flag and perms.
type file struct {
abs string
abs string // absolute file path, including root
dir string // containing directory of abs
rel string // relative to root, i.e. trim_prefix(abs, dir)
flag int
perm os.FileMode
}
// allowRead returns a new file{} for filepath permitted only to read.
func allowRead(filepath string) file {
return newFile(filepath, os.O_RDONLY, 0)
}
// allowCreate returns a new file{} for filepath permitted to read / write / create.
func allowCreate(filepath string) file {
return newFile(filepath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
}
// newFile returns a new instance of file{} for given path and open args.
func newFile(filepath string, flag int, perms os.FileMode) file {
dir, rel := path.Split(filepath)
return file{
abs: filepath,
rel: rel,
dir: dir,
flag: flag,
perm: perms,
}
}
// allowFiles implements fs.FS to allow
// access to a specified slice of files.
type allowFiles []file
@ -45,36 +72,32 @@ type allowFiles []file
// Open implements fs.FS.
func (af allowFiles) Open(name string) (fs.File, error) {
for _, file := range af {
var (
abs = file.abs
flag = file.flag
perm = file.perm
)
switch name {
// Allowed to open file
// at absolute path.
if name == file.abs {
return os.OpenFile(abs, flag, perm)
}
// at absolute path, or
// relative as ffmpeg likes.
case file.abs, file.rel:
return os.OpenFile(file.abs, file.flag, file.perm)
// Check for other valid reads.
thisDir, thisFile := path.Split(file.abs)
// Allowed to read directory itself.
if name == thisDir || name == "." {
return os.OpenFile(thisDir, flag, perm)
}
// Allowed to read file
// itself (at relative path).
if name == thisFile {
return os.OpenFile(abs, flag, perm)
// Ffmpeg likes to read containing
// dir as '.'. Allow RO access here.
case ".":
return openRead(file.dir)
}
}
return nil, os.ErrPermission
}
// openRead opens the existing file at path for reads only.
func openRead(path string) (*os.File, error) {
return os.OpenFile(path, os.O_RDONLY, 0)
}
// openWrite opens the (new!) file at path for read / writes.
func openWrite(path string) (*os.File, error) {
return os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
}
// getExtension splits file extension from path.
func getExtension(path string) string {
for i := len(path) - 1; i >= 0 && path[i] != '/'; i-- {
@ -93,17 +116,24 @@ func getExtension(path string) string {
// chance that Linux's sendfile syscall can be utilised for optimal
// draining of data source to temporary file storage.
func drainToTmp(rc io.ReadCloser) (string, error) {
defer rc.Close()
var tmp *os.File
var err error
// Close handles
// on func return.
defer func() {
tmp.Close()
rc.Close()
}()
// Open new temporary file.
tmp, err := os.CreateTemp(
os.TempDir(),
tmp, err = os.CreateTemp(
tmpdir,
"gotosocial-*",
)
if err != nil {
return "", err
}
defer tmp.Close()
// Extract file path.
path := tmp.Name()