even more defensive programming on image handling
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
parent
30a0e09a22
commit
607eaae3a9
5 changed files with 122 additions and 13 deletions
16
README.md
16
README.md
|
@ -13,12 +13,12 @@ Work in progress! Currently to do:
|
|||
- [X] RFC9110 compliant proxy loop detection with defensive programming against known vulnerable proxies
|
||||
- [X] HTTPs only mode and X-Forwarded-Proto reflection
|
||||
- [X] Cache-Control header
|
||||
- [X] Rate-limiting on local deployment (untested)
|
||||
- [X] Rate-limiting on local deployment
|
||||
- [X] Read config from Cloudflare
|
||||
- [X] Timing and Rate-limiting headers (some not available on Cloudflare Workers)
|
||||
- [X] Tiered rate-limiting
|
||||
- [ ] Lossy WebP on CF Workers (maybe already works?)
|
||||
- [ ] Cache Results on Cloudflare KV.
|
||||
- [ ] Cache results
|
||||
- [ ] Handle all possible panics reported by Clippy
|
||||
- [X] Sandboxing the image rendering
|
||||
- [X] Prometheus-format metrics
|
||||
|
@ -29,21 +29,29 @@ Work in progress! Currently to do:
|
|||
|
||||
Preview at:
|
||||
|
||||
[https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png)
|
||||
CF Worker: [https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png)
|
||||
|
||||
Local: [https://mproxy.mi.yumechi.jp/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png](https://mproxy.mi.yumechi.jp/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png)
|
||||
|
||||
Image:
|
||||
|
||||
![Syuilo Avatar resived.png](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png)
|
||||
|
||||
![Syuilo Avatar resived.png](https://mproxy.mi.yumechi.jp/proxy/avatar.webp?url=https://media.misskeyusercontent.com/io/274cc4f7-4674-4db1-9439-9fac08a66aa1.png)
|
||||
|
||||
|
||||
### SVG rendering
|
||||
|
||||
(font rendering disabled due to size restrictions)
|
||||
|
||||
[https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg)
|
||||
CF Worker: [https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg)
|
||||
|
||||
Local: [https://mproxy.mi.yumechi.jp/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg](https://mproxy.mi.yumechi.jp/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg)
|
||||
|
||||
![AES-AddRoundKey.svg](https://yumechi-no-kuni-proxy-worker.eternal-flame-ad.workers.dev/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg)
|
||||
|
||||
![AES-AddRoundKey.svg](https://mproxy.mi.yumechi.jp/proxy/static.webp?url=https://upload.wikimedia.org/wikipedia/commons/a/ad/AES-AddRoundKey.svg)
|
||||
|
||||
## Setup and Deployment
|
||||
|
||||
1. Clone this repository. Load the submodules with `git submodule update --init`.
|
||||
|
|
23
src/lib.rs
23
src/lib.rs
|
@ -813,6 +813,29 @@ pub struct App<C: UpstreamClient, S: Sandboxing> {
|
|||
_marker: PhantomData<(C, S)>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "env-local")]
|
||||
/// Register cancel handler for threads that has ran away for some reason
|
||||
///
|
||||
/// This should only happen when some kind of malformed input is happening that causes the image parser to hang
|
||||
/// without triggering the resource limits.
|
||||
#[allow(unsafe_code)]
|
||||
pub fn register_cancel_handler() {
|
||||
#[cfg(target_family = "unix")]
|
||||
unsafe {
|
||||
unsafe fn cancel_handler(_: libc::c_int) {
|
||||
log::error!("Received cancel signal, stopping thread");
|
||||
libc::pthread_exit(&sandbox::EXIT_TIMEOUT as *const _ as *mut _);
|
||||
}
|
||||
|
||||
if libc::signal(libc::SIGUSR1, cancel_handler as usize) == libc::SIG_ERR {
|
||||
log::error!(
|
||||
"Failed to register cancel handler: {}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "cf-worker", feature = "reqwest"))]
|
||||
#[allow(clippy::unused_async)]
|
||||
impl<C: UpstreamClient + 'static, S: Sandboxing + Send + Sync + 'static> App<C, S> {
|
||||
|
|
|
@ -225,6 +225,8 @@ fn main() {
|
|||
|
||||
log::info!("Spawned Tokio reactor.");
|
||||
|
||||
yumechi_no_kuni_proxy_worker::register_cancel_handler();
|
||||
|
||||
runtime
|
||||
.block_on(async move {
|
||||
#[cfg(not(feature = "axum-server"))]
|
||||
|
|
|
@ -2,7 +2,8 @@ use std::io::Cursor;
|
|||
|
||||
use image::{
|
||||
codecs::{png::PngDecoder, webp::WebPDecoder},
|
||||
AnimationDecoder, DynamicImage, GenericImageView, ImageResult,
|
||||
AnimationDecoder, DynamicImage, GenericImageView, ImageDecoder, ImageReader, ImageResult,
|
||||
Limits,
|
||||
};
|
||||
|
||||
use crate::ImageOptions;
|
||||
|
@ -34,7 +35,9 @@ pub fn postprocess_webp_image(
|
|||
data: &[u8],
|
||||
opt: &ImageOptions,
|
||||
) -> ImageResult<Option<DynamicImage>> {
|
||||
let dec = WebPDecoder::new(Cursor::new(data))?;
|
||||
let mut dec = WebPDecoder::new(Cursor::new(data))?;
|
||||
dec.set_limits(default_limits())
|
||||
.expect("Failed to set limits");
|
||||
|
||||
if !dec.has_animation() && opt.requested_resize() {
|
||||
return Ok(Some(postprocess_static_image(data, opt)?));
|
||||
|
@ -56,7 +59,9 @@ pub fn postprocess_webp_image(
|
|||
|
||||
/// Postprocesses an PNG image using the given options
|
||||
pub fn postprocess_png_image(data: &[u8], opt: &ImageOptions) -> ImageResult<Option<DynamicImage>> {
|
||||
let dec = PngDecoder::new(Cursor::new(data))?;
|
||||
let mut dec = PngDecoder::new(Cursor::new(data))?;
|
||||
dec.set_limits(default_limits())
|
||||
.expect("Failed to set limits");
|
||||
|
||||
let apng = dec.is_apng()?;
|
||||
|
||||
|
@ -81,6 +86,9 @@ pub fn postprocess_png_image(data: &[u8], opt: &ImageOptions) -> ImageResult<Opt
|
|||
#[derive(Debug, thiserror::Error)]
|
||||
/// An error that occurred during SVG postprocessing
|
||||
pub enum SvgPostprocessError {
|
||||
/// The complexity of the SVG image exceeded the limit
|
||||
#[error("SVG complexity limit exceeded: computed complexity is {0}")]
|
||||
LimitExceeded(usize),
|
||||
/// An error that occurred during rasterization
|
||||
#[error("Rasterization error: {0}")]
|
||||
Rasterization(#[from] image::ImageError),
|
||||
|
@ -123,6 +131,17 @@ pub fn postprocess_svg_image(
|
|||
},
|
||||
)?;
|
||||
|
||||
let complexity = svg.clip_paths().len()
|
||||
+ svg.filters().len()
|
||||
+ svg.linear_gradients().len()
|
||||
+ svg.radial_gradients().len()
|
||||
+ svg.patterns().len()
|
||||
+ (svg.size().width() * svg.size().height()) as usize / 1000;
|
||||
|
||||
if complexity > 25535 {
|
||||
return Err(SvgPostprocessError::LimitExceeded(complexity));
|
||||
}
|
||||
|
||||
let size = svg.size();
|
||||
let clamped = clamp_dimensions((size.width() as u32, size.height() as u32), 800, 800);
|
||||
|
||||
|
@ -144,8 +163,21 @@ pub fn postprocess_svg_image(
|
|||
))
|
||||
}
|
||||
|
||||
fn default_limits() -> Limits {
|
||||
let mut l = Limits::default();
|
||||
l.max_alloc = Some(64 << 20);
|
||||
l.max_image_height = Some(8192);
|
||||
l.max_image_width = Some(8192);
|
||||
|
||||
l
|
||||
}
|
||||
|
||||
/// Preprocesses a static image using the given options
|
||||
pub fn postprocess_static_image(data: &[u8], opt: &ImageOptions) -> ImageResult<DynamicImage> {
|
||||
let mut r = ImageReader::new(Cursor::new(data));
|
||||
|
||||
r.limits(default_limits());
|
||||
|
||||
Ok(process_static_image_impl(
|
||||
image::load_from_memory(data)?,
|
||||
opt,
|
||||
|
|
|
@ -1,7 +1,49 @@
|
|||
use crate::SandboxConfig;
|
||||
|
||||
#[cfg(all(target_family = "unix", feature = "tokio"))]
|
||||
/// Thread termination due to timeout
|
||||
pub const EXIT_TIMEOUT: usize = 1;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[allow(missing_docs)]
|
||||
pub(crate) mod pthread {
|
||||
|
||||
#[link(name = "pthread")]
|
||||
extern "C" {
|
||||
pub fn pthread_setcancelstate(state: i32, oldstate: *mut i32) -> i32;
|
||||
pub fn pthread_setcanceltype(type_: i32, oldtype: *mut i32) -> i32;
|
||||
}
|
||||
|
||||
pub const PTHREAD_CANCEL_ENABLE: i32 = 0;
|
||||
pub const PTHREAD_CANCEL_ASYNCHRONOUS: i32 = 1;
|
||||
}
|
||||
|
||||
/// Run a function with explicit immediate cancellation
|
||||
#[allow(unsafe_code)]
|
||||
pub(crate) fn pthread_cancelable<F: FnOnce() -> R, R>(f: F) -> R {
|
||||
unsafe {
|
||||
let mut oldstate = 0;
|
||||
let mut oldtype = 0;
|
||||
#[cfg(target_os = "linux")]
|
||||
pthread::pthread_setcancelstate(pthread::PTHREAD_CANCEL_ENABLE, &mut oldstate);
|
||||
#[cfg(target_os = "linux")]
|
||||
pthread::pthread_setcanceltype(pthread::PTHREAD_CANCEL_ASYNCHRONOUS, &mut oldtype);
|
||||
log::trace!(
|
||||
"Running cancelable function, oldstate: {}, oldtype: {}",
|
||||
oldstate,
|
||||
oldtype
|
||||
);
|
||||
let res = f();
|
||||
#[cfg(target_os = "linux")]
|
||||
pthread::pthread_setcancelstate(oldstate, std::ptr::null_mut());
|
||||
#[cfg(target_os = "linux")]
|
||||
pthread::pthread_setcanceltype(oldtype, std::ptr::null_mut());
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_family = "unix", feature = "tokio"))]
|
||||
#[allow(unsafe_code, clippy::inline_always)]
|
||||
#[inline(always)]
|
||||
/// A bloking tokio task with a cancellation
|
||||
pub fn tokio_block<F: FnOnce() -> R + Send, R: Send>(
|
||||
sandbox: &impl Sandboxing,
|
||||
|
@ -14,17 +56,19 @@ pub fn tokio_block<F: FnOnce() -> R + Send, R: Send>(
|
|||
tokio::spawn(async move {
|
||||
if signal.await.is_ok() {
|
||||
unsafe {
|
||||
libc::pthread_cancel(tid);
|
||||
libc::pthread_kill(tid, libc::SIGUSR1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tokio::task::block_in_place(move || {
|
||||
let guard = sandbox.setup(key);
|
||||
let res = f();
|
||||
drop(guard);
|
||||
pthread_cancelable(|| {
|
||||
let guard = sandbox.setup(key);
|
||||
let res = f();
|
||||
drop(guard);
|
||||
|
||||
res
|
||||
res
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue