Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

attempt to define brand check #30

Merged
merged 7 commits into from
Nov 27, 2018
Merged

attempt to define brand check #30

merged 7 commits into from
Nov 27, 2018

Conversation

codehag
Copy link
Contributor

@codehag codehag commented May 23, 2018

Thanks to David for helping me understand this a bit better. Would be great to get comments to improve this definition.

terminology.md Outdated
#### Definition:

Brand check ("brand" as in a mark, or a brand made with a branding iron) is a term used by the TC39
to describe a check against a unique datatype whose createion is controlled by a piece of code.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spelling -- createion -> creation (typing too fast!)

terminology.md Outdated
However, this is not limited to datatypes that are implemented as a part of JavaScript. Brand checks
are possible in user space as long as there is a way to identify that the object is unique.

Imagine a library that implements dom queries and returns a `query` object. The author of this
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dom -> DOM

terminology.md Outdated

set query(queryString) {
// generate a query object
const query = performQuery(queryString);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing a this.

terminology.md Outdated
class Query {
// ...

performQuery(queryString {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing paren

@dherman
Copy link
Member

dherman commented May 23, 2018

This looks awesome!

I'm not sure if the Date example is useful -- it might cause more confusion since it's a pretty obscure example of a brand. (Also getting it to work is more involved than just calling x.toString() -- you have to use Object.prototype.toString and call that on the object you're brand-checking.)

And I believe that after ES6 added @@toStringTag, it became possible to circumvent it, so it's not a reliable branding mechanism anymore.

So all of that confusing detail might make it not the most helpful example for a newcomer.

terminology.md Outdated
#### Example:

One example of this is built in JavaScript datatypes, which are unique and cannot be made in user
space. For example, `toString` can be used on the `new Date` object, and this is a unique identifier
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this example might not be great post-ES6, because of Symbol.toStringTag.

Array.isArray is a brand check, and Date.prototype.valueOf.call(date) will throw if date isn't a date object (iow it contains a brand check).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super! thanks for the examples!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah Array.isArray is a nice and more accessible example.

terminology.md Outdated

#### Sources
[esdiscuss comment](https://esdiscuss.org/topic/tostringtag-spoofing-for-null-and-undefined#content-3)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should link to forgery definition ?

@chancancode
Copy link

I think it would be helpful, as an intuition, to say something like "this is what you may think instanceof is for".

At least for me, the way I encountered this (and things like Array.isArray, etc) is having written code that does the instanceof check, and have someone point out to me why that doesn't work.

In the context of TC39, this also seems to come up for similar reasons.

@codehag
Copy link
Contributor Author

codehag commented May 23, 2018

@chancancode interesting, how was this described to you? then we can add that to the definition as well, because that sounds rather unintuitive

@chancancode
Copy link

(I'm in the middle of typing a long comment but we gotta go, will finish this tonight)

@allenwb
Copy link
Member

allenwb commented May 24, 2018

Note, that the actual brand mechanism for Date and most other builtins is the existence if a specific internal slot. The check is the invocation of any built in method that access the internal.

It might be worth mentioning is that branding in JS can't be reliability based upon inheritance-hierarchy tests such as instanceof

@chancancode
Copy link

chancancode commented May 24, 2018

This is probably TMI and needs to be condensed, but this is how I think about it (and I could be wrong).


Since JavaScript is a dynamically typed language, sometimes you may need to confirm that a value you received is of a particular type at runtime.

In order to do that, we first have to define what "of a particular type" means.

On one end of the spectrum, maybe all you care is that the value conforms to the interface you expect (i.e. it has the right properties and methods that you need for your usage). This is sometimes called a "structural" type check.

For example:

if (typeof value.toString === 'function') {
  // ...now we know value is a "ToString-able"
}

On the other end of the spectrum, sometimes that is not sufficient. You may need to know that the value is exactly what you expect. For example, you may have a method that should only work on arrays (the built-in type in JavaScript), not "array-like" objects such as an arguments object, DOM NodeList (the result of document.querySelectorAll() etc) or an object like ({ "0": ..., "1": ..., "length": 2, ... }).

This is sometimes called a "nominal" type check.

A "brand check" is an abstract operation that allows you to reliably determine that.


Why would you need to do that? Here are a few possible reasons:

  1. Checking whether a value is a built-in type.

  2. As a library author, you may want to provide an API that returns an object. Specifically, let's say you have an API that returns a result object (such as your query example). Further more, you have defined other functions that takes this result object as input. While all of these APIs are considered public, the internals of the result object is not. Keeping it opaque allows you to change the representation without breakage.

  3. Sometimes you could use brand checks to defend against certain threats.


For the first use case, let's say you want to implement prettyPrint. You may be tempted to write something like this:

function prettyPrint(v) {
  if (v instanceof Array) {
    let entries = v.map(prettyPrint);
    return `[${entries}]`;
  } else if (typeof v === "object") {
    let entries = Object.entries(v).map(([key, value]) => {
      return `${prettyKey(key)}: ${prettyValue(value)}`;
    }).join(", ");
    return `{${entries}}`;
  } else if (typeof v === "string") {
    return `"${v}"`;
  } else {
    return String(v);
  }
}

However, the v instanceof Array check could fail to detect what you may consider an array in some surprising edge cases. For example:

sameOriginIframe.contentWindow.eval("[]") instanceof Array // => false

The reason here is that instanceof checks if the right hand side is part of the left hand sides' prototype chain. However, since each execution context has it's own copy of these global objects:

sameOriginIframe.contentWindow.Array === Array // => false

...therefore the check fails.

A more reliable way to perform this check is Array.isArray(v), which will return true for all arrays regardless of where they came from.

One way to understand how this works is that it is implemented as a brand check: every time an array is created, it is "stamped" with an internal "brand" that only the engine has access to. Array.isArray simply checks if this stamp exists on the object that's passed in.

Seeing how this could be implemented in user-land may help. Suppose you want to implement a type that and you want to provide the same guarantee that it will work across iframes. (Another reason to care is that in Node it's pretty common to have multiple copies of the same library loaded, so an instanceof check will fail for objects that are created by a different copy of the library.)

This is one way you could implement that with a brand check instead of relying on instanceof:

// public constructor
export function moment(v) {
  if (isMoment(v)) {
    // already wrapped
    return v;
  } else {
    return new Moment(v);
  }
}

export function isMoment(v) {
  return typeof v === "object" && v._isMoment === true;
}

// internal
class Moment {
  constructor(v) {
    this._isMoment = true;
    // ...
  }
}

In this case, _isMoment is the "brand", and v._isMoment === true is the "brand check".


For the second use case, here is an alternative example that (I think) drives home the point a bit more directly.

Let's say we want to re-implement the setTimeout family APIs:

// internal
const callbackId = 0;
const callbacks = [];

function registerCallback(callback) {
  let id = callbackId++;
  callbacks[id] = callback;
  return id;
}

function unregisterCallback(id) {
  callbacks[id] = null;
}

export function setTimeout(callback) {
  return registerCallback(callback);
}

export function clearTimeout(id) {
  unregisterCallback(id);
}

export function setInterval(callback) {
  return registerCallback(callback);
}

export function clearInterval(id) {
  unregisterCallback(id);
}

As you can see, the public API is that setTimeout and setInterval takes a callback, and returns some kind of unique identifier that you can pass back to clearTimeout or clearInterval. This unique identifier is supposed to be opaque and not interchangeable (you should not be allowed to pass the id you got from setTimeout to clearTimeout or vice versa).

However, in our implementation, the unique identity happens to be a number. Furthermore, setTimeout and setInterval happens to share the same storage/id pool internally. Soon, someone will notice these incidental details and start relying on them in their code, and it becomes impossible to change your internal implementation details without breaking your users' code.

To avoid these problems, you can return a branded object from setTimeout and setInterval that only you can create, and do a brand check in clearTimeout and clearInterval to make sure that it originated from the right place.


For use case number 3, let's say we want to mark a certain value as "trusted". For example:

export function htmlSafe(value) {
  // ...return a branded wrapper
}

export function isHTMLSafe(value) {
  // ...check for the brand
}

export function render(value) {
  if (isHTMLSafe(value)) {
    document.body.innerHTML = value.toString();
  } else {
    document.body.innerText = value.toString();
  }
}

The key here is that you want to remember whether you have previous marked a particular value as "trusted".


It's probably important to point out that since "brand check" is an abstract operation, there are numerous ways to implement that, ranging from this._underscrorePoperty = true, Symbol, WeakMap, toStringTag, private state or using internal Magic™ ("internal slot") in native code.

Each of these offers different guarantees, and depending on the context it came up and the motivations for implementing the brand check, some of these may not be appropriate.

For example, if moment only cares about interop between execution contexts, they may not have to worry forgeability (if you intentionally do weird things like passing { _isMoment: true }, it's fine for you to get a runtime error), so an underscore property is perhaps appropriate.

On the other hand, if it security/privacy is a goal, you would have to define your threat model clearly and evaluate which implementation strategy is appropriate. For example, it's probably not acceptable to implement the htmlSafe brand check using an underscore property, as it would be fairly easy to produce an object that passes the check (e.g. from a JSON response).


In the context of TC39, the term may come up for a number of reasons.

In the most narrow sense (and perhaps the most "formal" sense), it usually describes an internal, cross-execution-context way to check for certain built-in types.

This could be useful for defining user-facing APIs, such as Array.isArray, but also internal operations such as IsPromise. This is important because the spec often need to communicate "this operation only works with type X" (e.g. "If IsPromise(promise) is false, throw a TypeError exception."). Since it is important for these brands to be invisible and unforgeable from userland, they are always modeled as an "internal slot" in the spec (but "internal slot" itself is an abstract construct that leaves plenty of room for different internal implementation/representation).

However, "branding" could also come up in other discussion context in a broader, more abstract sense. For example, it may come up when describing one of the aforementioned userland patterns (e.g. "How do developers solve X problem today? They use a brand check.") or discussing userland polyfills. In these cases, the concrete mechanism may or may not be important.


Once again, I think this is probably too long for the actual text you are writing, but hopefully it can be a useful input into that.

@codehag
Copy link
Contributor Author

codehag commented May 24, 2018

@chancancode thanks for the write up! I will link to your comment in the definition and create a short summary of what you said, but if someone wants more context then your comment will be really useful.

@codehag
Copy link
Contributor Author

codehag commented Jun 29, 2018

updated

terminology.md Outdated Show resolved Hide resolved
terminology.md Outdated Show resolved Hide resolved
terminology.md Outdated Show resolved Hide resolved
terminology.md Outdated Show resolved Hide resolved
rkirsling and others added 2 commits November 27, 2018 10:32
Co-Authored-By: codehag <yulia.startsev@gmail.com>
Co-Authored-By: codehag <yulia.startsev@gmail.com>
@codehag codehag merged commit 06f4fa8 into master Nov 27, 2018
@AlvinLaiPro

This comment has been minimized.

@ljharb ljharb deleted the define-brandcheck branch April 20, 2021 02:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants