# Chapter 20: Memory Safety in Web Apps

This chapter collects, bug class by bug class, what Loup does to keep a web
app correct: which mistakes the compiler refuses, which ones the runtime
catches, and which defences are still planned. Most of the examples are
mistakes you will actually make, shown with the message the Loup 1.25.1
toolchain prints and then fixed.

## The big picture

| Risk | Loup 1.25 today | Mechanism |
|---|---|---|
| Use-after-free, double free | Prevented | ARC semantics in every mode; no manual `free` |
| Missing value used as a value | Compile-time error | `Option[T]`; `None` is not a `string` |
| Out-of-range index | Run-time `IndexError` | Bounds-checked indexing |
| Wrong type through `Any` | Partly: `as?` checks, `as` does not yet | Checked casts |
| A callee changing data you lent it | Compile-time error | `reads` is the default verb |
| Hidden mutation at a call site | Compile-time error | `edits:` / `takes:` acknowledgements |
| Mutating method on shared object | Compile-time error | `modifier` + `safe {}` |
| Using a value after giving it away | Compile-time error | `takes`, `unique let` moves |
| Check-then-act race across `await` | Compile-time error | `atomic {}` refuses `await` |
| Leaked resources on error paths | Your code | `try` / `finally` |
| XSS through templates | **Your code** | `escape` filter by hand (auto-escaping ⚠ planned) |
| Integer overflow | **Not caught**: wraps | Specified as checked; not implemented |
| CSP, SSR state integrity, worker isolation, typed JS boundary | ⚠ Planned | No browser runtime ships yet |

All listings in this chapter use `@mode(web.compiled)` and run under
`loup run`, which executes a `main()` the way a compiled module's entry is run
(see [Chapter 2](ch02-compiled-mode-rules.md)).

## Memory: ARC semantics, not a garbage collector

Every Loup mode presents the same memory model: class instances are
reference-counted, and an object is released when the last reference to it
goes away (Language Reference §1-8.1.1; §1-2.6, footnote 1). The web modes
are no exception. On the Wasm target, Loup values are host-managed references
(`externref`) into the runtime's own heap. They are **not** Wasm GC objects;
compiling to Wasm GC and Exception Handling is a future target
(Language Reference §3-1).

What this gives you without any annotation:

- No `free`, so no use-after-free and no double free.
- Structs, enums, tuples and built collections are copied on assignment, so a
  function cannot keep a pointer into your list and change it later.
- An object's lifetime ends at a predictable point, which is when `del x`
  runs its `finalize()` method.

The one leak ARC allows is a reference cycle: two objects that point at each
other keep each other alive. The designed answer is a `weak` back-reference
(`pub weak var parent: Node?`).

> ⚠ **Planned.** `weak` fields compile in both web modes today, but the
> Language Reference disagrees with itself about them on the web: the feature
> matrix (§1-2.6) allows `weak` in `web.compiled`, while §3-1.2 forbids it
> there. Avoid parent pointers in web code until this is settled, or break
> cycles yourself by clearing the field.

## Null safety with `Option`

Loup has no `null`. A value that may be missing has type `Option[T]`, and a
plain `string` can never be `None`:

```loup,does_not_compile
@mode(web.compiled)

func main(): void {
    let userName: string = None;
    println("Hello, " + userName);
}
```

```text
main.lpweb:4:5: error[E0001]: cannot assign 'None' to binding 'userName' of type 'string'
```

Say in the type that the value can be absent, and the compiler makes every
reader deal with that case, either with a default (`??`) or with a `match`:

```loup
@mode(web.compiled)

func displayName(user: Option[string]): string {
    return user ?? "guest";
}

func main(): void {
    let signedIn: Option[string] = Some("ann");
    let anonymous: Option[string] = None;
    println(displayName(signedIn));
    println(displayName(anonymous));
    match anonymous {
        Some(name) => println("hello " + name),
        None => println("please sign in"),
    }
}
```

*Listing 20-1: Handling a missing user with `Option`*

```text
ann
guest
please sign in
```

`unwrap()` is still available when you are certain, and it is checked: on a
`None` it throws ``Uncaught UnwrapError: called `unwrap()` on a None value``
instead of reading garbage.

## Bounds-checked indexing

Every index is checked. Reading past the end of a list is a run-time error,
not a read of whatever memory follows:

```loup,throws
@mode(web.compiled)

func main(): void {
    let slides = ["intro", "demo", "outro"];
    let current = 3;
    println(slides[current]);
}
```

```text
Uncaught IndexError: list index 3 out of range (len 3)
  at main (main.lpweb:6:13)
```

In a UI, a stale index (a slide counter that ran one past the end, a selection
from a list that has since shrunk) is an ordinary event, not a bug that should
end the program. Turn the lookup into an `Option` and handle the missing case:

```loup
@mode(web.compiled)

func slideAt(slides: list[string], i: int): Option[string] {
    if (i < 0 or i >= slides.len()) {
        return None;
    }
    return Some(slides[i]);
}

func main(): void {
    let slides = ["intro", "demo", "outro"];
    let current = 3;
    match slideAt(slides, current) {
        Some(title) => println("showing " + title),
        None => println("no slide " + str(current)),
    }
    println(slideAt(slides, 1) ?? "end of deck");
    println(slideAt(slides, 3) ?? "end of deck");
}
```

*Listing 20-2: A bounds-checked lookup that returns `Option`*

```text
no slide 3
demo
end of deck
```

(`slides.get(i)` throws `IndexError` too; it does not return an `Option`.)

## Integer overflow wraps today

The Language Reference specifies checked integer arithmetic. Loup 1.25 does
not implement the check: `int` arithmetic wraps around silently, in the web
modes and everywhere else.

```loup,not_desired_behavior
@mode(web.compiled)

func main(): void {
    let big = 9223372036854775807;
    println(big + 1);
}
```

```text
-9223372036854775808
```

If a value can approach the `int` range (byte counters, money in the smallest
unit, IDs from a server), check the bound before the arithmetic.

## Type confusion through `Any`

In `web.compiled`, `Any` is allowed but costs you static checking; the compiler
warns with `W_ANY_IN_AOT_MODE` (§1-2.4.2). A form field arrives as `Any`, and
the obvious code casts it with `as`:

```loup,throws
@mode(web.compiled)

func quantity(raw: Any): int {
    return raw as int;
}

func main(): void {
    let total = quantity("3") + 1;
    println(total);
}
```

```text
main.lpweb:3:20: warning: W_ANY_IN_AOT_MODE: use of `Any` in web.compiled mode prevents AOT specialisation — consider a static type if performance is critical
main.lpweb:3:20: warning: W_ANY_IN_AOT_MODE: use of `Any` in web.compiled mode prevents AOT specialisation — consider a static type if performance is critical
Uncaught RuntimeError: internal interpreter limitation: type error: Add expects same numeric type, got Str("3") + Int(1)
```

The Language Reference says `expr as T` throws `TypeError` at the cast when
the value is not a `T` (§1-4.3). Stage0 does not check: the string `"3"`
passes through a function declared to return `int`, and the program fails
later, at the `+`, with an internal error. This is exactly the bug class
`Any` invites. Use `as?`, which returns `Option[T]` and does check, and
handle the `None`:

```loup
@mode(web.compiled)

func quantity(raw: Any): int {
    match raw as? int {
        Some(n) => return n,
        None => return 0,
    }
}

func main(): void {
    println(quantity(3) + 1);
    println(quantity("3") + 1);
}
```

```text
4
1
main.lpweb:3:20: warning: W_ANY_IN_AOT_MODE: use of `Any` in web.compiled mode prevents AOT specialisation — consider a static type if performance is critical
main.lpweb:3:20: warning: W_ANY_IN_AOT_MODE: use of `Any` in web.compiled mode prevents AOT specialisation — consider a static type if performance is critical
```

Better still, give the parameter a concrete type at the boundary and parse
once there. The `--warnings-as-errors=W_ANY_IN_AOT_MODE` switch described in
§1-2.4.2 does not exist in the 1.25.1 `loup run` (`error: unknown flag`).

## Cross-site scripting

An `html` fence does not escape inserted values in Loup 1.25 (Chapter 14
covers the details and the planned auto-escaping):

````loup,not_desired_behavior
@mode(web.compiled)

func main(): void {
    let comment = "<img src=x onerror=alert(1)>";
    let t = ```(html):(none)
    <li class="comment">{$ comment $}</li>
    ```;
    println(t.render({}));
}
````

```text
<li class="comment"><img src=x onerror=alert(1)></li>
```

A user who posts that comment runs script in every reader's browser. Escape
every untrusted value yourself:

````loup
@mode(web.compiled)

func main(): void {
    let comment = "<img src=x onerror=alert(1)>";
    let t = ```(html):(none)
    <li class="comment">{$ comment | escape $}</li>
    ```;
    println(t.render({}));
}
````

*Listing 20-3: Escaping a user comment*

```text
<li class="comment">&lt;img src=x onerror=alert(1)&gt;</li>
```

`escape` is enough for text and for quoted attribute values. It does not make
a `javascript:` URL safe, and nothing makes a value safe inside `<script>` or
`<style>`; Listing 14-9 shows a scheme check for links.

## Ownership verbs: who may change what

A parameter's *verb* says what the function may do with the argument
(Language Reference §3-2.4.9). The verbs work the same in every mode:

| Verb | The callee may | Call site writes |
|---|---|---|
| `reads` (default) | look at it | `f(x)` |
| `edits` | change it in place | `f(edits: x)` |
| `takes` | keep it; the caller loses it | `f(takes: x)` |

### `reads` is the default

A parameter with no verb is `reads`, so a function that only meant to count
items cannot quietly change the caller's list:

```loup,does_not_compile
@mode(web.compiled)

func countTags(tags: list[string]): int {
    tags.push("seen");
    return tags.len();
}

func main(): void {
    var tags = ["news"];
    println(countTags(tags));
}
```

```text
main.lpweb:4:5: error[E_READS_MUTATION]: E_READS_MUTATION: parameter 'tags' is a `reads` borrow (a parameter without a verb is read-only, §3-2.4.9) and cannot be mutated (`push` called on it); declare it `edits` and pass `edits: …` to change the caller's value, or copy it into a local first
```

The message offers both fixes. If the function should not change the list,
remove the `push`. If changing it is the point, say so with `edits`.

### The call site acknowledges `edits` and `takes`

Declaring `edits` is half of the contract. The caller must also write the
verb, so that a reader of the call can see the argument may change:

```loup,does_not_compile
@mode(web.compiled)

func addTag(edits tags: list[string], tag: string): void {
    tags.push(tag);
}

func main(): void {
    var tags = ["news"];
    addTag(tags, "sport");
    println(tags);
}
```

```text
main.lpweb:9:12: error[E_VERB_REQUIRED]: E_VERB_REQUIRED: parameter 'tags' requires 'edits' verb acknowledgment at call site
```

```loup
@mode(web.compiled)

func addTag(edits tags: list[string], tag: string): void {
    tags.push(tag);
}

func countTags(tags: list[string]): int {
    return tags.len();
}

func main(): void {
    var tags = ["news"];
    addTag(edits: tags, tag = "sport");
    println(tags);
    println(countTags(tags));
}
```

*Listing 20-4: `edits` at the declaration and at the call*

```text
["news", "sport"]
2
```

### Mutating methods: `modifier` and `safe`

A method that writes a `var` field of `this` must be declared `modifier`:

```loup,does_not_compile
@mode(web.compiled)

class Counter {
    pub var clicks: int = 0;
    pub func click(): void {
        this.clicks = this.clicks + 1;
    }
}

func main(): void {
    let c = Counter();
    c.click();
    println(c.clicks);
}
```

```text
main.lpweb:6:9: error[E_MODIFIER_REQUIRED]: E_MODIFIER_REQUIRED: writing to `var` field 'clicks' of `self` in method 'click' of class 'Counter' requires the method to be declared as `modifier`
```

Adding `modifier` moves the question to the caller. A class instance is a
shared, reference-counted object, so calling a mutating method on it must
happen in a `safe` context, where you take responsibility for there being no
other mutation in flight:

```loup,does_not_compile
@mode(web.compiled)

class Counter {
    pub var clicks: int = 0;
    pub modifier func click(): void {
        this.clicks = this.clicks + 1;
    }
}

func main(): void {
    let c = Counter();
    c.click();
    println(c.clicks);
}
```

```text
main.lpweb:12:5: error[E_SAFE_CONTEXT_REQUIRED]: E_SAFE_CONTEXT_REQUIRED: `modifier` method 'click' called on ARC receiver of type 'Counter' outside `safe {}` or `safe func` context — wrap the call in `safe { ... }` or mark the enclosing function as `safe func`
```

```loup
@mode(web.compiled)

class Counter {
    pub var clicks: int = 0;
    pub modifier func click(): void {
        this.clicks = this.clicks + 1;
    }
}

func main(): void {
    let c = Counter();
    safe {
        c.click();
        c.click();
    }
    println(c.clicks);
}
```

*Listing 20-5: A `modifier` method called inside `safe {}`*

```text
2
```

The same rules combine for a function that updates an object passed with
`edits`. In Stage0, writing a field through an `edits` parameter also needs
the function to be a `safe func` (the Language Reference §3-2.4.9 says `edits`
implies it), and a `safe func` is in turn called from a `safe` context:

```loup
@mode(web.compiled)

class Cart {
    pub var items: list[string] = [];
    pub var count: int = 0;
}

safe func addItem(edits cart: Cart, item: string): void {
    cart.items.push(item);
    cart.count = cart.count + 1;
}

func summary(cart: Cart): string {
    return str(cart.count) + " items";
}

func main(): void {
    let cart = Cart();
    safe {
        addItem(edits: cart, item = "tea");
        addItem(edits: cart, item = "milk");
    }
    println(summary(cart));
    println(cart.items);
}
```

*Listing 20-6: A shopping cart updated through `edits`*

```text
2 items
["tea", "milk"]
```

`summary` takes the cart with the default `reads`, and a reader can tell from
its signature alone that rendering the summary cannot change the cart.

### `takes`: handing a value over

`takes` transfers the argument to the callee. After the call, the caller's
binding is gone, which stops code from using an upload, a session or a
message that now belongs to someone else:

```loup,does_not_compile
@mode(web.compiled)

class Upload {
    pub var name: string;
    new(name: string) { this.name = name; }
}

func submit(takes u: Upload): string {
    return "sent " + u.name;
}

func main(): void {
    let u = Upload("photo.png");
    println(submit(takes: u));
    println(u.name);
}
```

```text
main.lpweb:15:13: error[E_USE_AFTER_TAKE]: E_USE_AFTER_TAKE: binding 'u' was consumed by a `takes` parameter and cannot be used again
```

Read what you still need before you hand the value over:

```loup
@mode(web.compiled)

class Upload {
    pub var name: string;
    new(name: string) { this.name = name; }
}

func submit(takes u: Upload): string {
    return "sent " + u.name;
}

func main(): void {
    let u = Upload("photo.png");
    let label = u.name;
    println(submit(takes: u));
    println(label);
}
```

```text
sent photo.png
photo.png
```

### `unique let`: one owner at a time

A `unique let` binding has a single owner. Assigning it to another binding
moves the value, and the old name can no longer be used:

```loup,does_not_compile
@mode(web.compiled)

class Token {
    pub let id: int;
}

func main(): void {
    unique let session = Token(id = 7);
    let handed = session;
    println(session.id);
}
```

```text
main.lpweb:10:13: error[E_USE_AFTER_MOVE]: E_USE_AFTER_MOVE: binding 'session' was moved and cannot be used again — `unique class` instances and move-only structs have single ownership
```

Use the binding that now owns the value (`println(handed.id);` prints `7`).

The stronger form, a `unique class` whose every instance is move-only, is not
available on the web. The feature matrix (§1-2.6) lists it for every mode, but
Stage0 accepts it only in `host.native` and `freestanding`:

```loup,does_not_compile
@mode(web.compiled)

unique class Session {
    pub let id: int;
    new(id: int) { this.id = id; }
}

func main(): void {
    let s = Session(1);
    println(s.id);
}
```

```text
main.lpweb:3:1: error[E_UNIQUE_CLASS_ARC_BINDING]: E_UNIQUE_CLASS_ARC_BINDING: `unique class 'Session'` is only valid in `@mode(native)` or `@mode(freestanding)`
```

(The message still uses the retired name `native`; the mode is
`host.native`.) In web code, use an ordinary `class` and `unique let` at the
bindings that need single ownership.

Two checks the Language Reference specifies are not performed yet:
`E_BORROW_ESCAPE` (a borrowed reference stored somewhere that outlives the
borrow) and `E_SAFE_ALIAS_MUTABLE`. Code that would trip them compiles today.

## Cleaning up on every path

A subscription, a timer or an open connection must be released even when the
code between acquiring and releasing it throws. `try` / `finally` guarantees
that:

```loup
@mode(web.compiled)

class Subscription {
    pub let topic: string;
    pub var open: bool = true;
    new(topic: string) {
        this.topic = topic;
        println("subscribe " + topic);
    }
    pub modifier func close(): void {
        this.open = false;
        println("unsubscribe " + this.topic);
    }
}

safe func render(topic: string): void {
    let sub = Subscription(topic);
    try {
        println("render " + topic);
        if (topic == "bad") {
            throw ValueError("render failed for " + topic);
        }
    } finally {
        sub.close();
    }
}

func main(): void {
    safe {
        render("prices");
        try {
            render("bad");
        } catch (ValueError as e) {
            println("caught: " + e.message);
        }
    }
}
```

*Listing 20-7: Releasing a subscription with `try` / `finally`*

```text
subscribe prices
render prices
unsubscribe prices
subscribe bad
render bad
unsubscribe bad
caught: render failed for bad
```

The `finally` block runs on the normal path and on the exception path.

## Check-then-act: `atomic {}`

On the browser's single thread, tasks interleave only where one of them
suspends at an `await`. A check followed by an action is a race if an `await`
sits between them: another task can change the state in that gap. An
`atomic {}` block is a region the compiler proves has no suspension point
(runtime §2-1.7):

```loup,does_not_compile
@mode(web.compiled)

class Stock {
    pub var left: int = 1;
    pub modifier func take(): void { this.left = this.left - 1; }
}

async func paymentOk(): Task[bool] {
    return true;
}

async func main(): Task[void] {
    let s = Stock();
    safe {
        atomic {
            if (s.left > 0) {
                let paid = await paymentOk();
                s.take();
            }
        }
    }
    println(s.left);
}
```

```text
main.lpweb:17:28: error[E_ATOMIC_AWAIT]: E_ATOMIC_AWAIT: await expression is not allowed inside atomic {} block
```

Do the waiting first, then check and act inside the block with nothing that
can suspend:

```loup
@mode(web.compiled)

class Stock {
    pub var left: int = 1;
    pub modifier func take(): void { this.left = this.left - 1; }
}

async func paymentOk(): Task[bool] {
    return true;
}

async func buy(edits s: Stock): Task[string] {
    let paid = await paymentOk();
    var result = "sold out";
    safe {
        atomic {
            if (paid and s.left > 0) {
                s.take();
                result = "reserved";
            }
        }
    }
    return result;
}

async func main(): Task[void] {
    let s = Stock();
    println(await buy(edits: s));
    println(await buy(edits: s));
}
```

*Listing 20-8: Await outside, check and act inside `atomic {}`*

```text
reserved
sold out
```

On the interpreter, `async` functions currently run eagerly and in order, so
no other task can actually slip into the gap yet; the `atomic {}` check is
static and holds regardless of how the scheduler runs (Chapter 15).

## Planned web defences

The remaining defences in the design belong to the browser runtime, which
Loup 1.25 does not ship. They are described in the chapters listed, all
marked ⚠ Planned:

- **Context-aware auto-escaping, `SafeHtml`, `Url`, the `<script>`/`<style>`
  prohibition** (Language Reference §1-16.6): Chapter 14.
- **Effect cleanup and component lifetimes** (`effect` returning a cleanup
  closure, signals owned by components; runtime §2-4.1): Chapter 16.
- **Shadow-DOM style isolation for Web Components** (runtime §2-4.10):
  Chapter 17.
- **Typed JS interop** (`extern js`, `@jsexport` with struct schemas checked
  at the boundary; runtime §2-3.1–§2-3.3) and **`Sendable` messages to Web
  Workers** (`ScriptWorker`, `WorkerChannel[T]`; runtime §2-1.10): Chapter 18.
- **SSR state serialization** that escapes `<` and `>` in the embedded JSON,
  the optional `stateIntegrity` SHA-256 check during hydration, and a strict
  Content Security Policy with `'wasm-unsafe-eval'` and no `'unsafe-inline'`
  (runtime §2-4.7): Chapter 19.

## A checklist for today

1. Pipe every untrusted value in an `html` fence through `| escape`; check URL
   schemes yourself; never interpolate into `<script>` or `<style>`.
2. Give possibly-missing values an `Option` type and handle `None`.
3. Treat an index from the outside world as untrusted: check it, or return an
   `Option`.
4. Avoid `Any`; where you must accept it, narrow with `as?`, not `as`.
5. Leave parameters `reads` unless the function must change them; mark the
   ones that must `edits`, and hand values over with `takes`.
6. Release resources in `finally`.
7. Keep `await` out of check-then-act sequences; wrap them in `atomic {}`.
8. Check bounds before arithmetic that could leave the `int` range: overflow
   wraps today.

## Summary

- Every Loup mode, including the web modes, presents ARC semantics. Wasm
  values are runtime-managed references, not Wasm GC objects.
- `Option` replaces `null`, indexing is bounds-checked, and `as?` narrows
  `Any` safely. Plain `as` does not check yet, and `int` overflow wraps.
- The verbs `reads` (default), `edits` and `takes`, the call-site
  acknowledgements, `modifier` / `safe`, and `unique let` turn aliasing and
  use-after-hand-over mistakes into compile errors: `E_READS_MUTATION`,
  `E_VERB_REQUIRED`, `E_MODIFIER_REQUIRED`, `E_SAFE_CONTEXT_REQUIRED`,
  `E_USE_AFTER_TAKE`, `E_USE_AFTER_MOVE`. `unique class` is not available in
  the web modes.
- `try` / `finally` releases resources on every path; `atomic {}` keeps
  `await` out of a check-then-act region (`E_ATOMIC_AWAIT`).
- `html` fences do not escape yet: use `| escape`. The browser-runtime
  defences (auto-escaping, CSP, SSR state integrity, worker `Sendable` checks,
  typed JS interop) are planned.
