librustc: Redo the unsafe checker and make unsafe methods not callable from safe code
This commit is contained in:
parent
b5da389d36
commit
aeda178011
@ -198,13 +198,15 @@ pub impl<T:Owned> MutexARC<T> {
|
||||
*/
|
||||
#[inline(always)]
|
||||
unsafe fn access<U>(&self, blk: &fn(x: &mut T) -> U) -> U {
|
||||
let state = self.x.get();
|
||||
// Borrowck would complain about this if the function were
|
||||
// not already unsafe. See borrow_rwlock, far below.
|
||||
do (&(*state).lock).lock {
|
||||
check_poison(true, (*state).failed);
|
||||
let _z = PoisonOnFail(&mut (*state).failed);
|
||||
blk(&mut (*state).data)
|
||||
unsafe {
|
||||
let state = self.x.get();
|
||||
// Borrowck would complain about this if the function were
|
||||
// not already unsafe. See borrow_rwlock, far below.
|
||||
do (&(*state).lock).lock {
|
||||
check_poison(true, (*state).failed);
|
||||
let _z = PoisonOnFail(&mut (*state).failed);
|
||||
blk(&mut (*state).data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -356,8 +358,8 @@ fn write_cond<'x, 'c, U>(&self,
|
||||
* access modes, this will not poison the ARC.
|
||||
*/
|
||||
fn read<U>(&self, blk: &fn(x: &T) -> U) -> U {
|
||||
let state = self.x.get();
|
||||
unsafe {
|
||||
let state = self.x.get();
|
||||
do (*state).lock.read {
|
||||
check_poison(false, (*state).failed);
|
||||
blk(&(*state).data)
|
||||
|
@ -100,30 +100,34 @@ fn new_sem_and_signal(count: int, num_condvars: uint)
|
||||
#[doc(hidden)]
|
||||
pub impl<Q:Owned> Sem<Q> {
|
||||
fn acquire(&self) {
|
||||
let mut waiter_nobe = None;
|
||||
do (**self).with |state| {
|
||||
state.count -= 1;
|
||||
if state.count < 0 {
|
||||
// Create waiter nobe.
|
||||
let (WaitEnd, SignalEnd) = comm::oneshot();
|
||||
// Tell outer scope we need to block.
|
||||
waiter_nobe = Some(WaitEnd);
|
||||
// Enqueue ourself.
|
||||
state.waiters.tail.send(SignalEnd);
|
||||
unsafe {
|
||||
let mut waiter_nobe = None;
|
||||
do (**self).with |state| {
|
||||
state.count -= 1;
|
||||
if state.count < 0 {
|
||||
// Create waiter nobe.
|
||||
let (WaitEnd, SignalEnd) = comm::oneshot();
|
||||
// Tell outer scope we need to block.
|
||||
waiter_nobe = Some(WaitEnd);
|
||||
// Enqueue ourself.
|
||||
state.waiters.tail.send(SignalEnd);
|
||||
}
|
||||
}
|
||||
// Uncomment if you wish to test for sem races. Not valgrind-friendly.
|
||||
/* for 1000.times { task::yield(); } */
|
||||
// Need to wait outside the exclusive.
|
||||
if waiter_nobe.is_some() {
|
||||
let _ = comm::recv_one(waiter_nobe.unwrap());
|
||||
}
|
||||
}
|
||||
// Uncomment if you wish to test for sem races. Not valgrind-friendly.
|
||||
/* for 1000.times { task::yield(); } */
|
||||
// Need to wait outside the exclusive.
|
||||
if waiter_nobe.is_some() {
|
||||
let _ = comm::recv_one(waiter_nobe.unwrap());
|
||||
}
|
||||
}
|
||||
fn release(&self) {
|
||||
do (**self).with |state| {
|
||||
state.count += 1;
|
||||
if state.count <= 0 {
|
||||
signal_waitqueue(&state.waiters);
|
||||
unsafe {
|
||||
do (**self).with |state| {
|
||||
state.count += 1;
|
||||
if state.count <= 0 {
|
||||
signal_waitqueue(&state.waiters);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -283,17 +287,19 @@ fn signal(&self) -> bool { self.signal_on(0) }
|
||||
|
||||
/// As signal, but with a specified condvar_id. See wait_on.
|
||||
fn signal_on(&self, condvar_id: uint) -> bool {
|
||||
let mut out_of_bounds = None;
|
||||
let mut result = false;
|
||||
do (**self.sem).with |state| {
|
||||
if condvar_id < state.blocked.len() {
|
||||
result = signal_waitqueue(&state.blocked[condvar_id]);
|
||||
} else {
|
||||
out_of_bounds = Some(state.blocked.len());
|
||||
unsafe {
|
||||
let mut out_of_bounds = None;
|
||||
let mut result = false;
|
||||
do (**self.sem).with |state| {
|
||||
if condvar_id < state.blocked.len() {
|
||||
result = signal_waitqueue(&state.blocked[condvar_id]);
|
||||
} else {
|
||||
out_of_bounds = Some(state.blocked.len());
|
||||
}
|
||||
}
|
||||
do check_cvar_bounds(out_of_bounds, condvar_id, "cond.signal_on()") {
|
||||
result
|
||||
}
|
||||
}
|
||||
do check_cvar_bounds(out_of_bounds, condvar_id, "cond.signal_on()") {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@ -304,20 +310,22 @@ fn broadcast(&self) -> uint { self.broadcast_on(0) }
|
||||
fn broadcast_on(&self, condvar_id: uint) -> uint {
|
||||
let mut out_of_bounds = None;
|
||||
let mut queue = None;
|
||||
do (**self.sem).with |state| {
|
||||
if condvar_id < state.blocked.len() {
|
||||
// To avoid :broadcast_heavy, we make a new waitqueue,
|
||||
// swap it out with the old one, and broadcast on the
|
||||
// old one outside of the little-lock.
|
||||
queue = Some(util::replace(&mut state.blocked[condvar_id],
|
||||
new_waitqueue()));
|
||||
} else {
|
||||
out_of_bounds = Some(state.blocked.len());
|
||||
unsafe {
|
||||
do (**self.sem).with |state| {
|
||||
if condvar_id < state.blocked.len() {
|
||||
// To avoid :broadcast_heavy, we make a new waitqueue,
|
||||
// swap it out with the old one, and broadcast on the
|
||||
// old one outside of the little-lock.
|
||||
queue = Some(util::replace(&mut state.blocked[condvar_id],
|
||||
new_waitqueue()));
|
||||
} else {
|
||||
out_of_bounds = Some(state.blocked.len());
|
||||
}
|
||||
}
|
||||
do check_cvar_bounds(out_of_bounds, condvar_id, "cond.signal_on()") {
|
||||
let queue = queue.swap_unwrap();
|
||||
broadcast_waitqueue(&queue)
|
||||
}
|
||||
}
|
||||
do check_cvar_bounds(out_of_bounds, condvar_id, "cond.signal_on()") {
|
||||
let queue = queue.swap_unwrap();
|
||||
broadcast_waitqueue(&queue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -262,6 +262,9 @@ pub fn compile_rest(sess: Session,
|
||||
time(time_passes, ~"privacy checking", ||
|
||||
middle::privacy::check_crate(ty_cx, &method_map, crate));
|
||||
|
||||
time(time_passes, ~"effect checking", ||
|
||||
middle::effect::check_crate(ty_cx, method_map, crate));
|
||||
|
||||
time(time_passes, ~"loop checking", ||
|
||||
middle::check_loop::check_crate(ty_cx, crate));
|
||||
|
||||
|
154
src/librustc/middle/effect.rs
Normal file
154
src/librustc/middle/effect.rs
Normal file
@ -0,0 +1,154 @@
|
||||
// Copyright 2012-2013 The Rust Project Developers. See the COPYRIGHT
|
||||
// file at the top-level directory of this distribution and at
|
||||
// http://rust-lang.org/COPYRIGHT.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
|
||||
//! Enforces the Rust effect system. Currently there is just one effect,
|
||||
/// `unsafe`.
|
||||
|
||||
use middle::ty::{ty_bare_fn, ty_closure, ty_ptr};
|
||||
use middle::ty;
|
||||
use middle::typeck::method_map;
|
||||
use util::ppaux;
|
||||
|
||||
use syntax::ast::{deref, expr_call, expr_inline_asm, expr_method_call};
|
||||
use syntax::ast::{expr_unary, node_id, unsafe_blk, unsafe_fn};
|
||||
use syntax::ast;
|
||||
use syntax::codemap::span;
|
||||
use syntax::visit::{fk_item_fn, fk_method};
|
||||
use syntax::visit;
|
||||
|
||||
#[deriving(Eq)]
|
||||
enum UnsafeContext {
|
||||
SafeContext,
|
||||
UnsafeFn,
|
||||
UnsafeBlock(node_id),
|
||||
}
|
||||
|
||||
struct Context {
|
||||
/// The method map.
|
||||
method_map: method_map,
|
||||
/// Whether we're in an unsafe context.
|
||||
unsafe_context: UnsafeContext,
|
||||
}
|
||||
|
||||
fn type_is_unsafe_function(ty: ty::t) -> bool {
|
||||
match ty::get(ty).sty {
|
||||
ty_bare_fn(ref f) => f.purity == unsafe_fn,
|
||||
ty_closure(ref f) => f.purity == unsafe_fn,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_crate(tcx: ty::ctxt,
|
||||
method_map: method_map,
|
||||
crate: @ast::crate) {
|
||||
let context = @mut Context {
|
||||
method_map: method_map,
|
||||
unsafe_context: SafeContext,
|
||||
};
|
||||
|
||||
let require_unsafe: @fn(span: span,
|
||||
description: &str) = |span, description| {
|
||||
match context.unsafe_context {
|
||||
SafeContext => {
|
||||
// Report an error.
|
||||
tcx.sess.span_err(span,
|
||||
fmt!("%s requires unsafe function or block",
|
||||
description))
|
||||
}
|
||||
UnsafeBlock(block_id) => {
|
||||
// OK, but record this.
|
||||
debug!("effect: recording unsafe block as used: %?", block_id);
|
||||
let _ = tcx.used_unsafe.insert(block_id);
|
||||
}
|
||||
UnsafeFn => {}
|
||||
}
|
||||
};
|
||||
|
||||
let visitor = visit::mk_vt(@visit::Visitor {
|
||||
visit_fn: |fn_kind, fn_decl, block, span, node_id, _, visitor| {
|
||||
let is_unsafe_fn = match *fn_kind {
|
||||
fk_item_fn(_, _, purity, _) => purity == unsafe_fn,
|
||||
fk_method(_, _, method) => method.purity == unsafe_fn,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let old_unsafe_context = context.unsafe_context;
|
||||
if is_unsafe_fn {
|
||||
context.unsafe_context = UnsafeFn
|
||||
}
|
||||
|
||||
visit::visit_fn(fn_kind,
|
||||
fn_decl,
|
||||
block,
|
||||
span,
|
||||
node_id,
|
||||
(),
|
||||
visitor);
|
||||
|
||||
context.unsafe_context = old_unsafe_context
|
||||
},
|
||||
|
||||
visit_block: |block, _, visitor| {
|
||||
let old_unsafe_context = context.unsafe_context;
|
||||
if block.node.rules == unsafe_blk {
|
||||
context.unsafe_context = UnsafeBlock(block.node.id)
|
||||
}
|
||||
|
||||
visit::visit_block(block, (), visitor);
|
||||
|
||||
context.unsafe_context = old_unsafe_context
|
||||
},
|
||||
|
||||
visit_expr: |expr, _, visitor| {
|
||||
match expr.node {
|
||||
expr_method_call(*) => {
|
||||
let base_type = ty::node_id_to_type(tcx, expr.callee_id);
|
||||
debug!("effect: method call case, base type is %s",
|
||||
ppaux::ty_to_str(tcx, base_type));
|
||||
if type_is_unsafe_function(base_type) {
|
||||
require_unsafe(expr.span,
|
||||
"invocation of unsafe method")
|
||||
}
|
||||
}
|
||||
expr_call(base, _, _) => {
|
||||
let base_type = ty::node_id_to_type(tcx, base.id);
|
||||
debug!("effect: call case, base type is %s",
|
||||
ppaux::ty_to_str(tcx, base_type));
|
||||
if type_is_unsafe_function(base_type) {
|
||||
require_unsafe(expr.span, "call to unsafe function")
|
||||
}
|
||||
}
|
||||
expr_unary(deref, base) => {
|
||||
let base_type = ty::node_id_to_type(tcx, base.id);
|
||||
debug!("effect: unary case, base type is %s",
|
||||
ppaux::ty_to_str(tcx, base_type));
|
||||
match ty::get(base_type).sty {
|
||||
ty_ptr(_) => {
|
||||
require_unsafe(expr.span,
|
||||
"dereference of unsafe pointer")
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
expr_inline_asm(*) => {
|
||||
require_unsafe(expr.span, "use of inline assembly")
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
visit::visit_expr(expr, (), visitor)
|
||||
},
|
||||
|
||||
.. *visit::default_visitor()
|
||||
});
|
||||
|
||||
visit::visit_crate(crate, (), visitor)
|
||||
}
|
||||
|
@ -891,21 +891,6 @@ fn mk_subr(&self,
|
||||
infer::mk_subr(self.infcx(), a_is_expected, span, sub, sup)
|
||||
}
|
||||
|
||||
fn require_unsafe(&self, sp: span, op: ~str) {
|
||||
match self.ps.purity {
|
||||
ast::unsafe_fn => {
|
||||
// ok, but flag that we used the source of unsafeness
|
||||
debug!("flagging %? as a used unsafe source", self.ps);
|
||||
self.tcx().used_unsafe.insert(self.ps.def);
|
||||
}
|
||||
_ => {
|
||||
self.ccx.tcx.sess.span_err(
|
||||
sp,
|
||||
fmt!("%s requires unsafe function or block", op));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn with_region_lb<R>(@mut self, lb: ast::node_id, f: &fn() -> R) -> R {
|
||||
let old_region_lb = self.region_lb;
|
||||
self.region_lb = lb;
|
||||
@ -2285,16 +2270,6 @@ fn check_loop_body(fcx: @mut FnCtxt,
|
||||
}
|
||||
ast::deref => {
|
||||
let sty = structure_of(fcx, expr.span, oprnd_t);
|
||||
match sty {
|
||||
// deref'ing an unsafe pointer requires that we be in
|
||||
// an unsafe context
|
||||
ty::ty_ptr(*) => {
|
||||
fcx.require_unsafe(
|
||||
expr.span,
|
||||
~"dereference of unsafe pointer");
|
||||
}
|
||||
_ => { /*ok*/ }
|
||||
}
|
||||
let operand_ty = ty::deref_sty(tcx, &sty, true);
|
||||
match operand_ty {
|
||||
Some(mt) => {
|
||||
@ -2392,8 +2367,6 @@ fn check_loop_body(fcx: @mut FnCtxt,
|
||||
fcx.write_ty(id, ty_param_bounds_and_ty.ty);
|
||||
}
|
||||
ast::expr_inline_asm(ref ia) => {
|
||||
fcx.require_unsafe(expr.span, ~"use of inline assembly");
|
||||
|
||||
for ia.inputs.each |&(_, in)| {
|
||||
check_expr(fcx, in);
|
||||
}
|
||||
@ -3223,13 +3196,6 @@ pub fn ty_param_bounds_and_ty_for_def(fcx: @mut FnCtxt,
|
||||
};
|
||||
}
|
||||
|
||||
ast::def_fn(id, ast::unsafe_fn) |
|
||||
ast::def_static_method(id, _, ast::unsafe_fn) => {
|
||||
// Unsafe functions can only be touched in an unsafe context
|
||||
fcx.require_unsafe(sp, ~"access to unsafe function");
|
||||
return ty::lookup_item_type(fcx.ccx.tcx, id);
|
||||
}
|
||||
|
||||
ast::def_fn(id, _) | ast::def_static_method(id, _, _) |
|
||||
ast::def_const(id) | ast::def_variant(_, id) |
|
||||
ast::def_struct(id) => {
|
||||
|
@ -109,6 +109,7 @@ pub mod middle {
|
||||
pub mod privacy;
|
||||
pub mod moves;
|
||||
pub mod entry;
|
||||
pub mod effect;
|
||||
}
|
||||
|
||||
pub mod front {
|
||||
|
@ -236,20 +236,24 @@ pub fn new(c: Chan<T>) -> SharedChan<T> {
|
||||
|
||||
impl<T: Owned> GenericChan<T> for SharedChan<T> {
|
||||
fn send(&self, x: T) {
|
||||
let mut xx = Some(x);
|
||||
do self.ch.with_imm |chan| {
|
||||
let x = replace(&mut xx, None);
|
||||
chan.send(x.unwrap())
|
||||
unsafe {
|
||||
let mut xx = Some(x);
|
||||
do self.ch.with_imm |chan| {
|
||||
let x = replace(&mut xx, None);
|
||||
chan.send(x.unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Owned> GenericSmartChan<T> for SharedChan<T> {
|
||||
fn try_send(&self, x: T) -> bool {
|
||||
let mut xx = Some(x);
|
||||
do self.ch.with_imm |chan| {
|
||||
let x = replace(&mut xx, None);
|
||||
chan.try_send(x.unwrap())
|
||||
unsafe {
|
||||
let mut xx = Some(x);
|
||||
do self.ch.with_imm |chan| {
|
||||
let x = replace(&mut xx, None);
|
||||
chan.try_send(x.unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -861,20 +861,18 @@ pub fn change_dir_locked(p: &Path, action: &fn()) -> bool {
|
||||
|
||||
fn key(_: Exclusive<()>) { }
|
||||
|
||||
let result = unsafe {
|
||||
global_data_clone_create(key, || {
|
||||
~exclusive(())
|
||||
})
|
||||
};
|
||||
unsafe {
|
||||
let result = global_data_clone_create(key, || { ~exclusive(()) });
|
||||
|
||||
do result.with_imm() |_| {
|
||||
let old_dir = os::getcwd();
|
||||
if change_dir(p) {
|
||||
action();
|
||||
change_dir(&old_dir)
|
||||
}
|
||||
else {
|
||||
false
|
||||
do result.with_imm() |_| {
|
||||
let old_dir = os::getcwd();
|
||||
if change_dir(p) {
|
||||
action();
|
||||
change_dir(&old_dir)
|
||||
}
|
||||
else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,16 +29,20 @@ pub fn new() -> MessageQueue<T> {
|
||||
}
|
||||
|
||||
pub fn push(&mut self, value: T) {
|
||||
let value = Cell(value);
|
||||
self.queue.with(|q| q.push(value.take()) );
|
||||
unsafe {
|
||||
let value = Cell(value);
|
||||
self.queue.with(|q| q.push(value.take()) );
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<T> {
|
||||
do self.queue.with |q| {
|
||||
if !q.is_empty() {
|
||||
Some(q.shift())
|
||||
} else {
|
||||
None
|
||||
unsafe {
|
||||
do self.queue.with |q| {
|
||||
if !q.is_empty() {
|
||||
Some(q.shift())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,32 +29,40 @@ fn new() -> WorkQueue<T> {
|
||||
}
|
||||
|
||||
fn push(&mut self, value: T) {
|
||||
let value = Cell(value);
|
||||
self.queue.with(|q| q.unshift(value.take()) );
|
||||
unsafe {
|
||||
let value = Cell(value);
|
||||
self.queue.with(|q| q.unshift(value.take()) );
|
||||
}
|
||||
}
|
||||
|
||||
fn pop(&mut self) -> Option<T> {
|
||||
do self.queue.with |q| {
|
||||
if !q.is_empty() {
|
||||
Some(q.shift())
|
||||
} else {
|
||||
None
|
||||
unsafe {
|
||||
do self.queue.with |q| {
|
||||
if !q.is_empty() {
|
||||
Some(q.shift())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn steal(&mut self) -> Option<T> {
|
||||
do self.queue.with |q| {
|
||||
if !q.is_empty() {
|
||||
Some(q.pop())
|
||||
} else {
|
||||
None
|
||||
unsafe {
|
||||
do self.queue.with |q| {
|
||||
if !q.is_empty() {
|
||||
Some(q.pop())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.queue.with_imm(|q| q.is_empty() )
|
||||
unsafe {
|
||||
self.queue.with_imm(|q| q.is_empty() )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -159,13 +159,17 @@ struct AncestorNode {
|
||||
// Accessors for taskgroup arcs and ancestor arcs that wrap the unsafety.
|
||||
#[inline(always)]
|
||||
fn access_group<U>(x: &TaskGroupArc, blk: &fn(TaskGroupInner) -> U) -> U {
|
||||
x.with(blk)
|
||||
unsafe {
|
||||
x.with(blk)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn access_ancestors<U>(x: &Exclusive<AncestorNode>,
|
||||
blk: &fn(x: &mut AncestorNode) -> U) -> U {
|
||||
x.with(blk)
|
||||
unsafe {
|
||||
x.with(blk)
|
||||
}
|
||||
}
|
||||
|
||||
// Iterates over an ancestor list.
|
||||
|
@ -19,5 +19,5 @@ mod test {
|
||||
|
||||
fn main() {
|
||||
test::free();
|
||||
//~^ ERROR access to unsafe function requires unsafe function or block
|
||||
//~^ ERROR call to unsafe function requires unsafe function or block
|
||||
}
|
||||
|
@ -19,5 +19,5 @@ mod test {
|
||||
|
||||
fn main() {
|
||||
let x = test::free;
|
||||
//~^ ERROR access to unsafe function requires unsafe function or block
|
||||
//~^ ERROR call to unsafe function requires unsafe function or block
|
||||
}
|
||||
|
@ -12,6 +12,6 @@
|
||||
|
||||
// Test that the `forget` and `init` intrinsics are really unsafe
|
||||
pub fn main() {
|
||||
let stuff = init::<int>(); //~ ERROR access to unsafe function requires unsafe
|
||||
forget(stuff); //~ ERROR access to unsafe function requires unsafe
|
||||
}
|
||||
let stuff = init::<int>(); //~ ERROR call to unsafe function requires unsafe
|
||||
forget(stuff); //~ ERROR call to unsafe function requires unsafe
|
||||
}
|
||||
|
@ -13,5 +13,5 @@
|
||||
unsafe fn f() { return; }
|
||||
|
||||
fn main() {
|
||||
f(); //~ ERROR access to unsafe function requires unsafe function or block
|
||||
f(); //~ ERROR call to unsafe function requires unsafe function or block
|
||||
}
|
||||
|
@ -13,6 +13,6 @@
|
||||
unsafe fn f() { return; }
|
||||
|
||||
fn main() {
|
||||
let x = f; //~ ERROR access to unsafe function requires unsafe function or block
|
||||
x();
|
||||
let x = f;
|
||||
x(); //~ ERROR call to unsafe function requires unsafe function or block
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user