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

[JENKINS-45892] Prevent model objects from being serialized except at top level #2957

Merged
merged 7 commits into from
Aug 8, 2017

Conversation

jglick
Copy link
Member

@jglick jglick commented Aug 1, 2017

See JENKINS-45892.

  • handle Run
  • handle AbstractItem
  • handle User

Proposed changelog entries

  • Jenkins will now prevent core or plugin code from mistakenly attempting to serialize jobs, builds, or users except in their intended top-level XML file positions, preventing a class of serious errors.

@reviewbybees

@jglick jglick added the work-in-progress The PR is under active development, not ready to the final review label Aug 1, 2017
return this;
} else {
// Unfortunately we cannot easily tell which XML file is actually being saved here, at least without implementing a custom Converter.
LOGGER.log(WARNING, "JENKINS-45892: reference to {0} being saved but not at top level", this);
Copy link
Member Author

Choose a reason for hiding this comment

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

Note that in the common case that you just have a [Run]Action storing a (nontransient) Run owner, writeReplace will get called only once—at top level—and we proceed normally. XStream then writes a reference="../../.." or similar, and deserialization works. We would rather print a warning in this case, but I could find no way to detect this case: AbstractReferenceMarshaller.convert runs and does not offer us any chance to intercept the process. This patch therefore only changes behavior in other cases:

  • A build stored via some reference chain in another build, as in the test.
  • A build stored in some unrelated object.
  • A build stored in another copy of itself.

The third is the problematic case for lazy-loading. I am not sure of the exact sequence of events (which is why I have no test for it), but somehow a build gets loaded, a badly written action is attached to it, the build is GC’d, a new Run object is reloaded from disk, but we have the original Action somewhere, and Run.save is called on one copy while another copy is referred to internally. Then instead of reference, XStream writes out a new nested <build> element. When deserialized, this explodes because onLoad is not called on the nested Run and so project is left null…as seen in in the toString patch.

@jglick jglick removed the work-in-progress The PR is under active development, not ready to the final review label Aug 1, 2017
@ghost
Copy link

ghost commented Aug 1, 2017

This pull request originates from a CloudBees employee. At CloudBees, we require that all pull requests be reviewed by other CloudBees employees before we seek to have the change accepted. If you want to learn more about our process please see this explanation.

Copy link
Member

@oleg-nenashev oleg-nenashev left a comment

Choose a reason for hiding this comment

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

Minor 🐛 for the replacer logic, but the approach generally looks good to me.

I would suggest moving the logic to the Restricted API classes, because there are use-case in other Jenkins model objects.

I also think a changelog entry is mandatory since the change implies a significant regression risk

synchronized (saving) {
if (saving.contains(this)) {
return this;
} else {
Copy link
Member

Choose a reason for hiding this comment

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

Why do you need to keep the else branch synchronized?

Copy link
Member Author

Choose a reason for hiding this comment

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

Could limit the monitor scope to the contains call, but should not matter—neither the logging nor the Replacer.<init> should block.

}
private Object readResolve() {
// May or may not work:
return Jenkins.getInstance().getItemByFullName(fullName);
Copy link
Member

Choose a reason for hiding this comment

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

From what I see this call may happen when Jenkins instance is null and when the jobs are not fully loaded. It would be better to handle this cases explicitly 🐛

Copy link
Member Author

Choose a reason for hiding this comment

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

It may indeed happen when jobs are not fully loaded, in which case the replacement is null, as seen in the test. It should not happen when Jenkins.instance == null because that would be before startup even, well, starts.

saving.add(this);
}
try {
getConfigFile().write(this);
Copy link
Member

Choose a reason for hiding this comment

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

code dup. would be better to move it to private saveInternal()

Copy link
Member Author

Choose a reason for hiding this comment

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

private? I guess you mean, public but @Restricted(NoExternalUse.class)?

Copy link
Member

Choose a reason for hiding this comment

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

fine as well

Copy link
Member

Choose a reason for hiding this comment

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

Should be final then though

saving.add(this);
}
try {
getConfigFile().write(this);
Copy link
Member

Choose a reason for hiding this comment

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

Would it be possible to move the save/replacer logic into a shared Impl class + Interface with default methods? It seems to be a common use-case for Jenkins which could be reused in other model objects. Having it in external class may help, especially when/if somebody starts working on pluggable storage stories in the core

Copy link
Member Author

Choose a reason for hiding this comment

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

Default methods are not an option since the saving list cannot be kept in an interface. Could be in a utility class somewhere.

Copy link
Member

Choose a reason for hiding this comment

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

Or JustAnotherMixIn contributed via interface

@jglick jglick added the needs-more-reviews Complex change, which would benefit from more eyes label Aug 2, 2017
@jglick jglick requested a review from oleg-nenashev August 3, 2017 16:12
Copy link
Member

@oleg-nenashev oleg-nenashev left a comment

Choose a reason for hiding this comment

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

🐝 resolver logic should work in such way. If not, we will get nice NPEs later at least

@jglick
Copy link
Member Author

jglick commented Aug 4, 2017

@reviewbybees done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-more-reviews Complex change, which would benefit from more eyes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants