Skip to content

Commit 4f19d68

Browse files
authored
Merge pull request #12620 from ethereum/assemblyAnnotation
Memory-safety annotation for inline assembly.
2 parents 81f1de2 + 6b6e163 commit 4f19d68

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+588
-26
lines changed

Changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
### 0.8.13 (unreleased)
22

33
Language Features:
4+
* General: Allow annotating inline assembly as memory-safe to allow optimizations and stack limit evasion that rely on respecting Solidity's memory model.
45

56

67
Compiler Features:

docs/assembly.rst

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,11 @@ of their block is reached.
228228
Conventions in Solidity
229229
-----------------------
230230

231+
.. _assembly-typed-variables:
232+
233+
Values of Typed Variables
234+
=========================
235+
231236
In contrast to EVM assembly, Solidity has types which are narrower than 256 bits,
232237
e.g. ``uint24``. For efficiency, most arithmetic operations ignore the fact that
233238
types can be shorter than 256
@@ -237,6 +242,11 @@ This means that if you access such a variable
237242
from within inline assembly, you might have to manually clean the higher-order bits
238243
first.
239244

245+
.. _assembly-memory-management:
246+
247+
Memory Management
248+
=================
249+
240250
Solidity manages memory in the following way. There is a "free memory pointer"
241251
at position ``0x40`` in memory. If you want to allocate memory, use the memory
242252
starting from where this pointer points at and update it.
@@ -268,3 +278,89 @@ first slot of the array and followed by the array elements.
268278
Statically-sized memory arrays do not have a length field, but it might be added later
269279
to allow better convertibility between statically- and dynamically-sized arrays, so
270280
do not rely on this.
281+
282+
Memory Safety
283+
=============
284+
285+
Without the use of inline assembly, the compiler can rely on memory to remain in a well-defined
286+
state at all times. This is especially relevant for :ref:`the new code generation pipeline via Yul IR <ir-breaking-changes>`:
287+
this code generation path can move local variables from stack to memory to avoid stack-too-deep errors and
288+
perform additional memory optimizations, if it can rely on certain assumptions about memory use.
289+
290+
While we recommend to always respect Solidity's memory model, inline assembly allows you to use memory
291+
in an incompatible way. Therefore, moving stack variables to memory and additional memory optimizations are,
292+
by default, disabled in the presence of any inline assembly block that contains a memory operation or assigns
293+
to solidity variables in memory.
294+
295+
However, you can specifically annotate an assembly block to indicate that it in fact respects Solidity's memory
296+
model as follows:
297+
298+
.. code-block:: solidity
299+
300+
/// @solidity memory-safe-assembly
301+
assembly {
302+
...
303+
}
304+
305+
In particular, a memory-safe assembly block may only access the following memory ranges:
306+
307+
- Memory allocated by yourself using a mechanism like the ``allocate`` function described above.
308+
- Memory allocated by Solidity, e.g. memory within the bounds of a memory array you reference.
309+
- The scratch space between memory offset 0 and 64 mentioned above.
310+
- Temporary memory that is located *after* the value of the free memory pointer at the beginning of the assembly block,
311+
i.e. memory that is "allocated" at the free memory pointer without updating the free memory pointer.
312+
313+
Furthermore, if the assembly block assigns to Solidity variables in memory, you need to assure that accesses to
314+
the Solidity variables only access these memory ranges.
315+
316+
Since this is mainly about the optimizer, these restrictions still need to be followed, even if the assembly block
317+
reverts or terminates. As an example, the following assembly snippet is not memory safe:
318+
319+
.. code-block:: solidity
320+
321+
assembly {
322+
returndatacopy(0, 0, returndatasize())
323+
revert(0, returndatasize())
324+
}
325+
326+
But the following is:
327+
328+
.. code-block:: solidity
329+
330+
/// @solidity memory-safe-assembly
331+
assembly {
332+
let p := mload(0x40)
333+
returndatacopy(p, 0, returndatasize())
334+
revert(p, returndatasize())
335+
}
336+
337+
Note that you do not need to update the free memory pointer if there is no following allocation,
338+
but you can only use memory starting from the current offset given by the free memory pointer.
339+
340+
If the memory operations use a length of zero, it is also fine to just use any offset (not only if it falls into the scratch space):
341+
342+
.. code-block:: solidity
343+
344+
/// @solidity memory-safe-assembly
345+
assembly {
346+
revert(0, 0)
347+
}
348+
349+
Note that not only memory operations in inline assembly itself can be memory-unsafe, but also assignments to
350+
solidity variables of reference type in memory. For example the following is not memory-safe:
351+
352+
.. code-block:: solidity
353+
354+
bytes memory x;
355+
assembly {
356+
x := 0x40
357+
}
358+
x[0x20] = 0x42;
359+
360+
Inline assembly that neither involves any operations that access memory nor assigns to any solidity variables
361+
in memory is automatically considered memory-safe and does not need to be annotated.
362+
363+
.. warning::
364+
It is your responsibility to make sure that the assembly actually satisfies the memory model. If you annotate
365+
an assembly block as memory-safe, but violate one of the memory assumptions, this **will** lead to incorrect and
366+
undefined behaviour that cannot easily be discovered by testing.

docs/ir-breaking-changes.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11

22
.. index: ir breaking changes
33
4+
.. _ir-breaking-changes:
5+
46
*********************************
57
Solidity IR-based Codegen Changes
68
*********************************

libsolidity/analysis/DocStringTagParser.cpp

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include <liblangutil/Common.h>
3131

3232
#include <range/v3/algorithm/any_of.hpp>
33+
#include <range/v3/view/filter.hpp>
3334

3435
#include <boost/algorithm/string.hpp>
3536

@@ -162,6 +163,71 @@ bool DocStringTagParser::visit(ErrorDefinition const& _error)
162163
return true;
163164
}
164165

166+
bool DocStringTagParser::visit(InlineAssembly const& _assembly)
167+
{
168+
if (!_assembly.documentation())
169+
return true;
170+
StructuredDocumentation documentation{-1, _assembly.location(), _assembly.documentation()};
171+
ErrorList errors;
172+
ErrorReporter errorReporter{errors};
173+
auto docTags = DocStringParser{documentation, errorReporter}.parse();
174+
175+
if (!errors.empty())
176+
{
177+
SecondarySourceLocation ssl;
178+
for (auto const& error: errors)
179+
if (error->comment())
180+
ssl.append(
181+
*error->comment(),
182+
_assembly.location()
183+
);
184+
m_errorReporter.warning(
185+
7828_error,
186+
_assembly.location(),
187+
"Inline assembly has invalid NatSpec documentation.",
188+
ssl
189+
);
190+
}
191+
192+
for (auto const& [tagName, tagValue]: docTags)
193+
{
194+
if (tagName == "solidity")
195+
{
196+
vector<string> values;
197+
boost::split(values, tagValue.content, isWhiteSpace);
198+
199+
set<string> valuesSeen;
200+
set<string> duplicates;
201+
for (auto const& value: values | ranges::views::filter(not_fn(&string::empty)))
202+
if (valuesSeen.insert(value).second)
203+
{
204+
if (value == "memory-safe-assembly")
205+
_assembly.annotation().markedMemorySafe = true;
206+
else
207+
m_errorReporter.warning(
208+
8787_error,
209+
_assembly.location(),
210+
"Unexpected value for @solidity tag in inline assembly: " + value
211+
);
212+
}
213+
else if (duplicates.insert(value).second)
214+
m_errorReporter.warning(
215+
4377_error,
216+
_assembly.location(),
217+
"Value for @solidity tag in inline assembly specified multiple times: " + value
218+
);
219+
}
220+
else
221+
m_errorReporter.warning(
222+
6269_error,
223+
_assembly.location(),
224+
"Unexpected NatSpec tag \"" + tagName + "\" with value \"" + tagValue.content + "\" in inline assembly."
225+
);
226+
}
227+
228+
return true;
229+
}
230+
165231
void DocStringTagParser::checkParameters(
166232
CallableDeclaration const& _callable,
167233
StructurallyDocumented const& _node,

libsolidity/analysis/DocStringTagParser.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class DocStringTagParser: private ASTConstVisitor
4848
bool visit(ModifierDefinition const& _modifier) override;
4949
bool visit(EventDefinition const& _event) override;
5050
bool visit(ErrorDefinition const& _error) override;
51+
bool visit(InlineAssembly const& _assembly) override;
5152

5253
void checkParameters(
5354
CallableDeclaration const& _callable,

libsolidity/analysis/TypeChecker.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,7 @@ void TypeChecker::endVisit(FunctionTypeName const& _funType)
763763

764764
bool TypeChecker::visit(InlineAssembly const& _inlineAssembly)
765765
{
766+
bool lvalueAccessToMemoryVariable = false;
766767
// External references have already been resolved in a prior stage and stored in the annotation.
767768
// We run the resolve step again regardless.
768769
yul::ExternalIdentifierAccess::Resolver identifierAccess = [&](
@@ -787,6 +788,8 @@ bool TypeChecker::visit(InlineAssembly const& _inlineAssembly)
787788
if (auto var = dynamic_cast<VariableDeclaration const*>(declaration))
788789
{
789790
solAssert(var->type(), "Expected variable type!");
791+
if (_context == yul::IdentifierContext::LValue && var->type()->dataStoredIn(DataLocation::Memory))
792+
lvalueAccessToMemoryVariable = true;
790793
if (var->immutable())
791794
{
792795
m_errorReporter.typeError(3773_error, nativeLocationOf(_identifier), "Assembly access to immutable variables is not supported.");
@@ -974,8 +977,11 @@ bool TypeChecker::visit(InlineAssembly const& _inlineAssembly)
974977
identifierAccess
975978
);
976979
if (!analyzer.analyze(_inlineAssembly.operations()))
977-
return false;
978-
return true;
980+
solAssert(m_errorReporter.hasErrors());
981+
_inlineAssembly.annotation().hasMemoryEffects =
982+
lvalueAccessToMemoryVariable ||
983+
(analyzer.sideEffects().memory != yul::SideEffects::None);
984+
return false;
979985
}
980986

981987
bool TypeChecker::visit(IfStatement const& _ifStatement)

libsolidity/ast/ASTAnnotations.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,10 @@ struct InlineAssemblyAnnotation: StatementAnnotation
220220
std::map<yul::Identifier const*, ExternalIdentifierInfo> externalReferences;
221221
/// Information generated during analysis phase.
222222
std::shared_ptr<yul::AsmAnalysisInfo> analysisInfo;
223+
/// True, if the assembly block was annotated to be memory-safe.
224+
bool markedMemorySafe = false;
225+
/// True, if the assembly block involves any memory opcode or assigns to variables in memory.
226+
SetOnce<bool> hasMemoryEffects;
223227
};
224228

225229
struct BlockAnnotation: StatementAnnotation, ScopableAnnotation

libsolidity/codegen/ir/IRGenerationContext.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,8 @@ class IRGenerationContext
160160

161161
std::set<ContractDefinition const*, ASTNode::CompareByID>& subObjectsCreated() { return m_subObjects; }
162162

163-
bool inlineAssemblySeen() const { return m_inlineAssemblySeen; }
164-
void setInlineAssemblySeen() { m_inlineAssemblySeen = true; }
163+
bool memoryUnsafeInlineAssemblySeen() const { return m_memoryUnsafeInlineAssemblySeen; }
164+
void setMemoryUnsafeInlineAssemblySeen() { m_memoryUnsafeInlineAssemblySeen = true; }
165165

166166
/// @returns the runtime ID to be used for the function in the dispatch routine
167167
/// and for internal function pointers.
@@ -202,8 +202,8 @@ class IRGenerationContext
202202
/// Whether to use checked or wrapping arithmetic.
203203
Arithmetic m_arithmetic = Arithmetic::Checked;
204204

205-
/// Flag indicating whether any inline assembly block was seen.
206-
bool m_inlineAssemblySeen = false;
205+
/// Flag indicating whether any memory-unsafe inline assembly block was seen.
206+
bool m_memoryUnsafeInlineAssemblySeen = false;
207207

208208
/// Function definitions queued for code generation. They're the Solidity functions whose calls
209209
/// were discovered by the IR generator during AST traversal.

libsolidity/codegen/ir/IRGenerator.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,8 @@ string IRGenerator::generate(
213213
t("subObjects", subObjectSources(m_context.subObjectsCreated()));
214214

215215
// This has to be called only after all other code generation for the creation object is complete.
216-
bool creationInvolvesAssembly = m_context.inlineAssemblySeen();
217-
t("memoryInitCreation", memoryInit(!creationInvolvesAssembly));
216+
bool creationInvolvesMemoryUnsafeAssembly = m_context.memoryUnsafeInlineAssemblySeen();
217+
t("memoryInitCreation", memoryInit(!creationInvolvesMemoryUnsafeAssembly));
218218
t("useSrcMapCreation", formatUseSrcMap(m_context));
219219

220220
resetContext(_contract, ExecutionContext::Deployed);
@@ -239,8 +239,8 @@ string IRGenerator::generate(
239239
t("useSrcMapDeployed", formatUseSrcMap(m_context));
240240

241241
// This has to be called only after all other code generation for the deployed object is complete.
242-
bool deployedInvolvesAssembly = m_context.inlineAssemblySeen();
243-
t("memoryInitDeployed", memoryInit(!deployedInvolvesAssembly));
242+
bool deployedInvolvesMemoryUnsafeAssembly = m_context.memoryUnsafeInlineAssemblySeen();
243+
t("memoryInitDeployed", memoryInit(!deployedInvolvesMemoryUnsafeAssembly));
244244

245245
solAssert(_contract.annotation().creationCallGraph->get() != nullptr, "");
246246
solAssert(_contract.annotation().deployedCallGraph->get() != nullptr, "");

libsolidity/codegen/ir/IRGeneratorForStatements.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2138,7 +2138,8 @@ void IRGeneratorForStatements::endVisit(MemberAccess const& _memberAccess)
21382138
bool IRGeneratorForStatements::visit(InlineAssembly const& _inlineAsm)
21392139
{
21402140
setLocation(_inlineAsm);
2141-
m_context.setInlineAssemblySeen();
2141+
if (*_inlineAsm.annotation().hasMemoryEffects && !_inlineAsm.annotation().markedMemorySafe)
2142+
m_context.setMemoryUnsafeInlineAssemblySeen();
21422143
CopyTranslate bodyCopier{_inlineAsm.dialect(), m_context, _inlineAsm.annotation().externalReferences};
21432144

21442145
yul::Statement modified = bodyCopier(_inlineAsm.operations());

0 commit comments

Comments
 (0)