-
Notifications
You must be signed in to change notification settings - Fork 33
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
fix: Ponyfill should not tamper with globals #47
Conversation
Prior to this change, the ponyfill module would also polyfill Symbol.observable. With this change, the ponyfill can be used in environments with immutable primordials.
root.Symbol = root.Symbol || function Symbol(description) { | ||
return '@@' + description; | ||
}; | ||
root.Symbol.observable = result; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will still fail if Symbol
(a primordial) is frozen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are two entry points. This is the polyfill (mutates globals) and the other is a ponyfill (the other module) is the subset that exports its API and does not mutate globals.
@@ -16,4 +16,10 @@ if (typeof self !== 'undefined') { | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably not for this PR, but I suggest adding a globalThis
branch to the above if-else sequence.
@@ -1,17 +1,23 @@ | |||
|
|||
const observableSymbols = new WeakMap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the following simpler module accomplish the same thing without reference to root
or use of WeakMap?
export default Symbol('observable');
where index.js line 18 would just be
import result from 'ponyfill.js';
If not, I don't understand what we're trying to accomplish.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The WeakMap
was only necessary to make this work with the unit test, which runs the ponyfill multiple times in the same execution environment. If I were to reframe the tests such that they run the ponyfill only once per process, it would be sufficient to capture the result
in a module scope singleton.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want the same reference to a symbol
, then Symbol(description)
won't do that, but we don't need the WeakMap
. We could use Symbol.for('symbol-observable-ponyfill')
or the like, and have a unique, but shared symbol
. (So long as Symbol.for
exists, which it likely does if Symbol
exists).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know that two evaluations of Symbol('observable')
will give two separate symbols, which is not what we want. But (under current conditions) a module has one instance, so all importers should get the same symbol.
Yes, using Symbol.for('observable')
would be a more robust way to ensure that there is only one such symbol.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’ll take another look at this with an eye for removing the WeakMap
and using Symbol.for
to ensure idempotence instead.
@@ -16,4 +16,10 @@ if (typeof self !== 'undefined') { | |||
} | |||
|
|||
var result = ponyfill(root); | |||
|
|||
root.Symbol = root.Symbol || function Symbol(description) { | |||
return '@@' + description; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only problem I see with this is that it doesn't align with what Symbol
does.
With Symbol(description)
the description doesn't affect the equality of the result. In other words: Symbol('test') !== Symbol('test')
... This is different than Symbol.for
, which does what you're expecting this to do.
The result of Symbol
must be a completely unique value. The @@
helps, and is an accepted practice. But really, I guess you'd need to do something like:
let _symbolNumber = 0;
function Symbol(description) {
return `@@_${_symbolNumber++}_${description}`
}
The other depressing bit about this, is you'd have to make sure that description
was converted into a valid set of characters for a property name. OR you'd have to just attach the description to the string you returned somehow (although I haven't thought much about how).
Since we're polyfilling Symbol
in this case, I guess I'd expect it to behave appropriately (and even possibly implement Symbol.for
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, and the code I’m modifying presumes the existence of Symbol
or a Symbol
shim as you describe. That being the case, I think it would be sufficient for parity to simply remove this line. Does that track?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could remove that line yeah... However, I'm not sure if there's a runtime that exists where WeakMap
exists, but Symbol
does not. I'm trying to research that now.
So I have two questions:
- Can you elaborate on the problem you're trying to solve?
- Can we move the creation of the
WeakMap
inside of the check for existence ofSymbol
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess, if I'm understanding right, in a runtime you're using calling Symbol.observable = ???
will throw an error because Symbol
is frozen. Is that correct?
If that's the case, could we just wrap it in a try-catch?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could remove the WeakMap
but I would have to make a more invasive change to the test, either by exporting a clearForRetest
function or running each test in a fresh process.
Capturing the failed assignment to Symbol.observable
is an option. Checking the property descriptor for writability would be another. This is still my recommendation since it clearly separates the polyfill from the ponyfill. I’ve really just moved the assignment to Symbol.observable
from the ponyfill to the polyfill. I’ll write up whatever solution you prefer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if there's a runtime that exists where WeakMap exists, but Symbol does not. I'm trying to research that now.
They were both introduced in ES6, so I would be surprised. Please let us know what you find. I'm curious.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess, if I'm understanding right, in a runtime you're using calling
Symbol.observable = ???
will throw an error becauseSymbol
is frozen. Is that correct?
Yes.
If that's the case, could we just wrap it in a try-catch?
And do what in the catch clause? Whatever that is, if it works in that case, why not just do that all the time?
@@ -33,7 +33,6 @@ describe('ponyfill unit tests', function () { | |||
|
|||
var result = ponyfill(root); | |||
|
|||
expect(Symbol.observable).to.equal('Symbol(observable)'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should remove this, as if Symbol
exists, we're expecting to have polyfilled it, but if it doesn't exist, we at least have the returned value.
if (typeof Symbol === 'function') { | ||
if (Symbol.observable) { | ||
result = Symbol.observable; | ||
} else { | ||
result = Symbol('observable'); | ||
Symbol.observable = result; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, I'm not sure if I understand the goal of removing the polyfilling from this?
Is it that it causes an error in some runtimes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. With the XS runtime and with SES, which both protect client code from prototype pollution, Symbol is a frozen object. My understanding is that the useful distinction between a ponyfill and a polyfill is that the former is useful in such environments and doesn’t require the special treatment of shims.
I should provide more context: I’m coming with this change request because there is a cryptoeconomic project that would benefit from protection against supply chain attacks. That project largely works under SES, but three of its modules depend on this ponyfill. |
Okay, so I think the "ponyfill" aspect of this is maybe a little bit off... when I inherited this repo from @sindresorhus, the ponyfill would also polyfill if it could: https://github.com/benlesh/symbol-observable/blob/1738bfc9fefef23d310dba3d68e0d83d44253265/index.js So perhaps that is the source of some of the confusion here. BUT... I think we can solve this in this PR.
Do you think that will solve your issues? |
I think so as well, since
yes.
Assigning to a non-writable property does throw in strict-mode code, and therefore in module code. It only does not throw in sloppy mode code.
The try/catch would work, leaving a system in which the correct symbol is importable but not on
AFAICT, yes. |
@kriskowal, @erights, please review the following for me? |
I believe the changes in #48 do the job. Thank you! |
@benlesh For your consideration,
Prior to this change, the ponyfill module would also polyfill Symbol.observable. With this change, the ponyfill can be used in environments with immutable primordials.
Please let me know if this change is acceptable, or if I can adjust it to be consistent with this project’s style.