blob: 3be9efecc2c62b4a511bf3de01b5116909111669 [file] [log] [blame]
/**
* Checks whether member access or array casting is allowed in `@safe` code.
*
* Specification: $(LINK2 https://dlang.org/spec/function.html#function-safety, Function Safety)
*
* Copyright: Copyright (C) 1999-2025 by The D Language Foundation, All Rights Reserved
* Authors: $(LINK2 https://www.digitalmars.com, Walter Bright)
* License: $(LINK2 https://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
* Source: $(LINK2 https://github.com/dlang/dmd/blob/master/compiler/src/dmd/safe.d, _safe.d)
* Documentation: https://dlang.org/phobos/dmd_safe.html
* Coverage: https://codecov.io/gh/dlang/dmd/src/master/compiler/src/dmd/safe.d
*/
module dmd.safe;
import core.stdc.stdio;
import dmd.aggregate;
import dmd.astenums;
import dmd.common.outbuffer;
import dmd.dcast : implicitConvTo;
import dmd.dclass;
import dmd.declaration;
import dmd.dscope;
import dmd.dsymbol;
import dmd.dsymbolsem : determineSize;
import dmd.errors;
import dmd.expression;
import dmd.func;
import dmd.funcsem : isRootTraitsCompilesScope;
import dmd.globals : FeatureState, global;
import dmd.id;
import dmd.identifier;
import dmd.location;
import dmd.mtype;
import dmd.rootobject;
import dmd.root.string : fTuple;
import dmd.target;
import dmd.tokens;
import dmd.typesem : hasPointers, arrayOf, size;
/*************************************************************
* Check for unsafe access in @safe code:
* 1. read overlapped pointers
* 2. write misaligned pointers
* 3. write overlapped storage classes
* Print error if unsafe.
* Params:
* sc = scope
* e = expression to check
* readonly = if access is read-only
* printmsg = print error message if true
* Returns:
* true if error
*/
bool checkUnsafeAccess(Scope* sc, Expression e, bool readonly, bool printmsg)
{
//printf("checkUnsafeAccess(e: '%s', readonly: %d, printmsg: %d)\n", e.toChars(), readonly, printmsg);
if (e.op != EXP.dotVariable)
return false;
auto dve = cast(DotVarExp)e;
VarDeclaration v = dve.var.isVarDeclaration();
if (!v)
return false;
if (!sc.func)
return false;
auto ad = v.isMember2();
if (!ad)
return false;
if (v.isSystem())
{
if (sc.setUnsafePreview(sc.previews.systemVariables, !printmsg, e.loc,
"accessing `@system` field `%s.%s`", ad, v))
return true;
}
// This branch shouldn't be here, but unfortunately calling `ad.determineSize`
// breaks code with circular reference errors. Specifically, test23589.d fails
if (ad.sizeok != Sizeok.done && !sc.func.isSafeBypassingInference())
return false;
// needed to set v.overlapped and v.overlapUnsafe
if (ad.sizeok != Sizeok.done)
ad.determineSize(ad.loc);
import dmd.globals : FeatureState;
const hasPointers = v.type.hasPointers();
if (hasPointers)
{
if (v.overlapped)
{
if (sc.func.isSafeBypassingInference() && sc.setUnsafe(!printmsg, e.loc,
"accessing overlapped field `%s.%s` with pointers", ad, v))
{
return true;
}
else
{
// @@@DEPRECATED_2.116@@@
// https://issues.dlang.org/show_bug.cgi?id=20655
// Inferring `@system` because of union access breaks code,
// so make it a deprecation safety violation as of 2.106
// To turn into an error, remove `isSafeBypassingInference` check in the
// above if statement and remove the else branch
sc.setUnsafePreview(FeatureState.default_, !printmsg, e.loc,
"accessing overlapped field `%s.%s` with pointers", ad, v);
}
}
}
if (v.type.hasInvariant())
{
if (v.overlapped)
{
if (sc.setUnsafe(!printmsg, e.loc,
"accessing overlapped field `%s.%s` with a structs invariant",
ad, v))
return true;
}
}
// @@@DEPRECATED_2.119@@@
// https://issues.dlang.org/show_bug.cgi?id=24477
// Should probably be turned into an error in a new edition
if (v.type.hasUnsafeBitpatterns() && v.overlapped && sc.setUnsafePreview(
FeatureState.default_, !printmsg, e.loc,
"accessing overlapped field `%s.%s` with unsafe bit patterns", ad, v)
)
{
return true;
}
if (readonly || !e.type.isMutable())
return false;
if (hasPointers && v.type.toBasetype().ty != Tstruct)
{
if ((!ad.type.alignment.isDefault() && ad.type.alignment.get() < target.ptrsize) ||
(v.offset & (target.ptrsize - 1)))
{
if (sc.setUnsafe(!printmsg, e.loc,
"modifying misaligned pointers through field `%s.%s`", ad, v))
return true;
}
}
if (v.overlapUnsafe)
{
if (sc.setUnsafe(!printmsg, e.loc,
"modifying field `%s.%s` which overlaps with fields with other storage classes",
ad, v))
{
return true;
}
}
return false;
}
/**********************************************
* Determine if it is @safe to cast e from tfrom to tto.
* Params:
* e = expression to be cast
* tfrom = type of e
* tto = type to cast e to
* msg = reason why cast is unsafe or deprecated
* Returns:
* true if @safe or deprecated
*/
bool isSafeCast(Expression e, Type tfrom, Type tto, ref string msg)
{
// Implicit conversions are always safe
if (tfrom.implicitConvTo(tto))
return true;
if (!tto.hasPointers())
return true;
auto tfromb = tfrom.toBasetype();
auto ttob = tto.toBasetype();
// Casting to void* is always safe, https://github.com/dlang/dmd/issues/20514
if (ttob.isTypePointer() && ttob.nextOf().toBasetype().ty == Tvoid)
return true;
if (ttob.ty == Tclass && tfromb.ty == Tclass)
{
ClassDeclaration cdfrom = tfromb.isClassHandle();
ClassDeclaration cdto = ttob.isClassHandle();
int offset;
if (cdfrom == cdto)
goto Lsame;
if (!cdfrom.isBaseOf(cdto, &offset) &&
!((cdfrom.isInterfaceDeclaration() || cdto.isInterfaceDeclaration())
&& cdfrom.classKind == ClassKind.d && cdto.classKind == ClassKind.d))
{
msg = "Source object type is incompatible with target type";
return false;
}
// no RTTI
if (cdfrom.isCPPinterface() || cdto.isCPPinterface())
{
msg = "No dynamic type information for extern(C++) classes";
return false;
}
if (cdfrom.classKind == ClassKind.cpp || cdto.classKind == ClassKind.cpp)
msg = "No dynamic type information for extern(C++) classes";
Lsame:
if (!MODimplicitConv(tfromb.mod, ttob.mod))
{
msg = "Incompatible type qualifier";
return false;
}
return true;
}
if (ttob.ty == Tarray && tfromb.ty == Tsarray) // https://issues.dlang.org/show_bug.cgi?id=12502
tfromb = tfromb.nextOf().arrayOf();
if (ttob.ty == Tarray && tfromb.ty == Tarray ||
ttob.ty == Tpointer && tfromb.ty == Tpointer)
{
Type ttobn = ttob.nextOf().toBasetype();
Type tfromn = tfromb.nextOf().toBasetype();
/* From void[] to anything mutable is unsafe because:
* int*[] api;
* void[] av = api;
* int[] ai = cast(int[]) av;
* ai[0] = 7;
* *api[0] crash!
*/
if (tfromn.ty == Tvoid && ttobn.isMutable())
{
if (ttob.ty == Tarray && e.op == EXP.arrayLiteral)
return true;
msg = "`void` data may contain pointers and target element type is mutable";
return false;
}
// If the struct is opaque we don't know about the struct members then the cast becomes unsafe
if (ttobn.ty == Tstruct && !(cast(TypeStruct)ttobn).sym.members)
{
msg = "Target element type is opaque";
return false;
}
if (tfromn.ty == Tstruct && !(cast(TypeStruct)tfromn).sym.members)
{
msg = "Source element type is opaque";
return false;
}
if (e.op != EXP.arrayLiteral)
{
// For bool, only 0 and 1 are safe values
// Runtime array cast reinterprets data
if (ttobn.ty == Tbool && tfromn.ty != Tbool)
msg = "Source element may have bytes which are not 0 or 1";
else if (ttobn.hasUnsafeBitpatterns())
msg = "Target element type has unsafe bit patterns";
// Can't alias a bool pointer with a non-bool pointer
if (ttobn.ty != Tbool && tfromn.ty == Tbool && ttobn.isMutable())
msg = "Target element could be assigned a byte which is not 0 or 1";
else if (tfromn.hasUnsafeBitpatterns() && ttobn.isMutable())
msg = "Source element type has unsafe bit patterns and target element type is mutable";
}
const frompointers = tfromn.hasPointers();
const topointers = ttobn.hasPointers();
if (frompointers && !topointers && ttobn.isMutable())
{
msg = "Target element type is mutable and source element type contains a pointer";
return false;
}
if (!frompointers && topointers)
{
msg = "Target element type contains a pointer";
return false;
}
if (!topointers &&
ttobn.ty != Tfunction && tfromn.ty != Tfunction &&
(ttob.ty == Tarray || ttobn.size() <= tfromn.size()) &&
MODimplicitConv(tfromn.mod, ttobn.mod))
{
return true;
}
}
msg = "Source type is incompatible with target type containing a pointer";
return false;
}
/*************************************************
* Check for unsafe use of `.ptr` or `.funcptr`
* Params:
* sc = context
* e = expression for error messages
* id = `ptr` or `funcptr`
* flag = DotExpFlag
* Returns:
* true if error
*/
bool checkUnsafeDotExp(Scope* sc, Expression e, Identifier id, int flag)
{
if (!(flag & DotExpFlag.noDeref)) // this use is attempting a dereference
{
if (id == Id.ptr)
return sc.setUnsafe(false, e.loc, "using `%s.ptr` (instead of `&%s[0])`", e, e);
return sc.setUnsafe(false, e.loc, "using `%s.%s`", e, id);
}
return false;
}
/**************************************
* Safer D adds safety checks to functions with the default
* trust setting.
*/
bool isSaferD(FuncDeclaration fd)
{
return fd.type.toTypeFunction().trust == TRUST.default_ && fd.saferD;
}
bool isSafe(FuncDeclaration fd)
{
if (fd.safetyInprocess)
setFunctionToUnsafe(fd);
return fd.type.toTypeFunction().trust == TRUST.safe;
}
extern (D) bool isSafeBypassingInference(FuncDeclaration fd)
{
return !(fd.safetyInprocess) && fd.isSafe();
}
bool isTrusted(FuncDeclaration fd)
{
if (fd.safetyInprocess)
setFunctionToUnsafe(fd);
return fd.type.toTypeFunction().trust == TRUST.trusted;
}
/*****************************************************
* Report safety violation for function `fd`, or squirrel away
* error message in fd.safetyViolation if needed later.
* Call when `fd` was just inferred to be @system OR
* `fd` was @safe and an tried something unsafe.
* Params:
* fd = function we're gonna rat on
* gag = suppress error message (used in escape.d)
* loc = location of error
* format = printf-style format string
* args = arguments for %s format specifier
*/
extern (D) void reportSafeError(FuncDeclaration fd, bool gag, Loc loc,
const(char)* format, RootObject[] args...)
{
if (fd.type.toTypeFunction().trust == TRUST.system) // function was just inferred to be @system
{
if (format)
{
fd.safetyViolation = new AttributeViolation(loc, format, args);
}
else if (args.length > 0)
{
if (FuncDeclaration fd2 = (cast(Dsymbol) args[0]).isFuncDeclaration())
{
fd.safetyViolation = new AttributeViolation(loc, fd2); // call to non-@nogc function
}
}
}
else if (fd.isSafe() || fd.isSaferD())
{
if (!gag && format)
{
OutBuffer buf;
buf.writestring(AttributeViolation(loc, format, args).action);
if (fd.isSafe())
buf.writestring(" is not allowed in a `@safe` function");
else
{
version (IN_GCC)
buf.writestring(" is not allowed in a function with default safety with `-fpreview=safer`");
else
buf.writestring(" is not allowed in a function with default safety with `-preview=safer`");
}
.error(loc, "%s", buf.extractChars());
}
}
}
/**********************************************
* Function is doing something unsafe. If inference
* is in process, commit the function to be @system.
* Params:
* fd = the naughty function
* Returns:
* true if this is a safe function and so an error OR is inferred to be @system,
* false otherwise.
*/
extern (D) bool setFunctionToUnsafe(FuncDeclaration fd)
{
if (fd.safetyInprocess)
{
fd.safetyInprocess = false;
fd.type.toTypeFunction().trust = TRUST.system;
if (fd.fes)
setFunctionToUnsafe(fd.fes.func);
return true;
}
else if (fd.isSafe() || fd.isSaferD())
return true;
return false;
}
/**************************************
* The function is calling `@system` function `f`, so mark it as unsafe.
*
* Params:
* fd = caller
* f = function being called (needed for diagnostic of inferred functions)
* Returns: whether there's a safe error
*/
extern (D) bool setUnsafeCall(FuncDeclaration fd, FuncDeclaration f)
{
if (setFunctionToUnsafe(fd))
{
reportSafeError(fd, false, f.loc, null, f, null);
return fd.isSafe();
}
return false;
}
/**************************************
* A statement / expression in this scope is not `@safe`,
* so mark the enclosing function as `@system`
*
* Params:
* sc = scope that the unsafe statement / expression is in
* gag = surpress error message (used in escape.d)
* loc = location of error
* format = printf-style format string
* args = arguments for format string
* Returns: whether there is a safe error
*/
bool setUnsafe(Scope* sc, bool gag, Loc loc, const(char)* format, RootObject[] args...)
{
if (sc.intypeof)
return false; // typeof(cast(int*)0) is safe
if (sc.debug_) // debug {} scopes are permissive
return false;
if (!sc.func)
{
if (sc.varDecl)
{
if (sc.varDecl.storage_class & STC.safe)
{
string action = AttributeViolation(loc, format, args).action;
.error(loc, "%.*s can't initialize `@safe` variable `%s`", action.fTuple.expand, sc.varDecl.toChars());
return true;
}
else if (!(sc.varDecl.storage_class & STC.trusted))
{
sc.varDecl.storage_class |= STC.system;
sc.varDecl.systemInferred = true;
}
}
return false;
}
if (isRootTraitsCompilesScope(sc)) // __traits(compiles, x)
{
if (sc.func.isSafeBypassingInference())
{
// Message wil be gagged, but still call error() to update global.errors and for
// -verrors=spec
string action = AttributeViolation(loc, format, args).action;
.error(loc, "%.*s is not allowed in a `@safe` function", action.fTuple.expand);
return true;
}
return false;
}
if (setFunctionToUnsafe(sc.func))
{
if (format || args.length > 0)
{
reportSafeError(sc.func, gag, loc, format, args);
}
return sc.func.isSafe(); // it is only an error if in an @safe function
}
return false;
}
/***************************************
* Like `setUnsafe`, but for safety errors still behind preview switches
*
* Given a `FeatureState fs`, for example dip1000 / dip25 / systemVariables,
* the behavior changes based on the setting:
*
* - In case of `-revert=fs`, it does nothing.
* - In case of `-preview=fs`, it's the same as `setUnsafe`
* - By default, print a deprecation in `@safe` functions, or store an attribute violation in inferred functions.
*
* Params:
* sc = used to find affected function/variable, and for checking whether we are in a deprecated / speculative scope
* fs = feature state from the preview flag
* gag = surpress error message
* loc = location of error
* format = printf-style format string
* args = arguments for format string
* Returns: whether an actual safe error (not deprecation) occured
*/
bool setUnsafePreview(Scope* sc, FeatureState fs, bool gag, Loc loc, const(char)* format, RootObject[] args...)
{
//printf("setUnsafePreview() fs:%d %s\n", fs, fmt);
assert(format);
with (FeatureState) final switch (fs)
{
case disabled:
return false;
case enabled:
return sc.setUnsafe(gag, loc, format, args);
case default_:
if (!sc.func)
return false;
if (sc.func.isSafeBypassingInference())
{
if (!gag && !sc.isDeprecated())
{
string action = AttributeViolation(loc, format, args).action;
deprecation(loc, "%.*s will become `@system` in a future release", action.fTuple.expand);
}
}
else if (!sc.func.safetyViolation)
{
import dmd.func : AttributeViolation;
sc.func.safetyViolation = new AttributeViolation(loc, format, args);
}
return false;
}
}