Skip to content

Commit 883dc6d

Browse files
committed
HADOOP-19668 Add SubjectInheritingThread to restore pre JDK22 Subject behaviour in Threads
1 parent 960a2ef commit 883dc6d

File tree

2 files changed

+310
-0
lines changed

2 files changed

+310
-0
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.hadoop.util.concurrent;
20+
21+
import java.security.PrivilegedAction;
22+
import javax.security.auth.Subject;
23+
24+
import org.apache.hadoop.security.authentication.util.SubjectUtil;
25+
26+
/**
27+
* Helper class to restore Subject propagation behavior of threads after the
28+
* JEP411/JEP486 changes.
29+
* <p>
30+
* Java propagates the current Subject to any new Threads in all version up to
31+
* Java 21. In Java 22-23 the Subject is only propagated if the SecurityManager
32+
* is enabled, while in Java 24+ it is never propagated.
33+
* <p>
34+
* Hadoop security heavily relies on the original behavior, as Subject is at the
35+
* core of JAAS. This class wraps thread. It overrides start() and saves the
36+
* Subject of the current thread, and wraps the payload in a
37+
* Subject.doAs()/callAs() call to restorere it in the newly created Thread.
38+
* <p>
39+
* When specifying a Runnable, this class is used in exactly the same way as
40+
* Thread.
41+
* <p>
42+
* {@link #run()} cannot be directly overridden, as that would also override the
43+
* subject restoration logic. SubjectInheritingThread provides a {@link work()}
44+
* method instead, which is wrapped and invoked by its own final {@link run()}
45+
* method.
46+
*/
47+
public class SubjectInheritingThread extends Thread {
48+
49+
private Subject startSubject;
50+
// {@link Thread#target} is private, so we need our own
51+
private Runnable hadoopTarget;
52+
53+
/**
54+
* Behaves similar to {@link Thread#Thread()} constructor, but the code to run
55+
* must be specified by overriding the {@link #work()} instead of the {link
56+
* #run()} method.
57+
*/
58+
public SubjectInheritingThread() {
59+
super();
60+
}
61+
62+
/**
63+
* Behaves similar to {@link Thread#Thread(Runnable)} constructor.
64+
*/
65+
public SubjectInheritingThread(Runnable target) {
66+
super();
67+
this.hadoopTarget = target;
68+
}
69+
70+
/**
71+
* Behaves similar to {@link Thread#Thread(ThreadGroup, Runnable)} constructor.
72+
*/
73+
public SubjectInheritingThread(ThreadGroup group, Runnable target) {
74+
// The target passed to Thread has no effect, we only pass it
75+
// because there is no super(group) constructor.
76+
super(group, target);
77+
this.hadoopTarget = target;
78+
}
79+
80+
/**
81+
* Behaves similar to {@link Thread#Thread(Runnable, String)} constructor.
82+
*/
83+
public SubjectInheritingThread(Runnable target, String name) {
84+
super(name);
85+
this.hadoopTarget = target;
86+
}
87+
88+
/**
89+
* Behaves similar to {@link Thread#Thread(String)} constructor.
90+
*/
91+
public SubjectInheritingThread(String name) {
92+
super(name);
93+
}
94+
95+
/**
96+
* Behaves similar to {@link Thread#Thread(ThreadGroup, String)} constructor.
97+
*/
98+
public SubjectInheritingThread(ThreadGroup group, String name) {
99+
super(group, name);
100+
}
101+
102+
/**
103+
* Behaves similar to {@link Thread#Thread(ThreadGroup, Runnable, String)}
104+
* constructor.
105+
*/
106+
public SubjectInheritingThread(ThreadGroup group, Runnable target, String name) {
107+
super(group, name);
108+
this.hadoopTarget = target;
109+
}
110+
111+
/**
112+
* Behaves similar to pre-Java 22 {@link Thread#start()}. It saves the current
113+
* Subject before starting the new thread, which is then used as the Subject for
114+
* the Runnable or the overridden work() method.
115+
*/
116+
@Override
117+
public final void start() {
118+
startSubject = SubjectUtil.current();
119+
super.start();
120+
}
121+
122+
/**
123+
* This is the equivalent of {@link Thread#run()}. Override this instead of
124+
* {@link #run()} Subject will be propagated like in pre-Java 22 Thread.
125+
*/
126+
public void work() {
127+
if (hadoopTarget != null) {
128+
hadoopTarget.run();
129+
}
130+
}
131+
132+
/**
133+
* This cannot be overridden in this class. Override the {@link #work()} method
134+
* instead which behaves like pre-Java 22 {@link Thread#run()}
135+
*/
136+
@Override
137+
public final void run() {
138+
SubjectUtil.doAs(startSubject, new PrivilegedAction<Void>() {
139+
140+
@Override
141+
public Void run() {
142+
work();
143+
return null;
144+
}
145+
146+
});
147+
}
148+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
* <p>
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
* <p>
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.hadoop.util.concurrent;
20+
21+
import static org.junit.jupiter.api.Assertions.assertEquals;
22+
import static org.junit.jupiter.api.Assertions.assertNull;
23+
24+
import java.util.concurrent.Callable;
25+
26+
import javax.security.auth.Subject;
27+
28+
import org.apache.hadoop.security.authentication.util.SubjectUtil;
29+
import org.apache.hadoop.util.Daemon;
30+
import org.apache.hadoop.util.Shell;
31+
import org.junit.jupiter.api.Test;
32+
33+
public class TestSubjectPropagation {
34+
35+
private Subject childSubject = null;
36+
37+
@Test
38+
public void testSubjectInheritingThreadOverride() {
39+
Subject parentSubject = new Subject();
40+
childSubject = null;
41+
42+
SubjectUtil.callAs(parentSubject, new Callable<Void>() {
43+
public Void call() throws InterruptedException {
44+
SubjectInheritingThread t = new SubjectInheritingThread() {
45+
@Override
46+
public void work() {
47+
childSubject = SubjectUtil.current();
48+
}
49+
};
50+
t.start();
51+
t.join(1000);
52+
return (Void) null;
53+
}
54+
});
55+
56+
assertEquals(parentSubject, childSubject);
57+
}
58+
59+
@Test
60+
public void testSubjectInheritingThreadRunnable() {
61+
Subject parentSubject = new Subject();
62+
childSubject = null;
63+
64+
SubjectUtil.callAs(parentSubject, new Callable<Void>() {
65+
public Void call() throws InterruptedException {
66+
Runnable r = new Runnable() {
67+
@Override
68+
public void run() {
69+
childSubject = SubjectUtil.current();
70+
}
71+
};
72+
73+
SubjectInheritingThread t = new SubjectInheritingThread(r);
74+
t.start();
75+
t.join(1000);
76+
return (Void) null;
77+
}
78+
});
79+
80+
assertEquals(parentSubject, childSubject);
81+
}
82+
83+
@Test
84+
public void testThreadOverride() {
85+
Subject parentSubject = new Subject();
86+
childSubject = null;
87+
88+
SubjectUtil.callAs(parentSubject, new Callable<Void>() {
89+
public Void call() throws InterruptedException {
90+
91+
Thread t = new Thread() {
92+
@Override
93+
public void run() {
94+
childSubject = SubjectUtil.current();
95+
}
96+
};
97+
t.start();
98+
t.join(1000);
99+
return (Void) null;
100+
}
101+
});
102+
103+
boolean securityManagerEnabled = true;
104+
try {
105+
SecurityManager sm = System.getSecurityManager();
106+
System.setSecurityManager(sm);
107+
} catch (UnsupportedOperationException e) {
108+
// JDK24+ always throws this
109+
securityManagerEnabled = false;
110+
} catch (Throwable t) {
111+
// don't care
112+
}
113+
114+
if (Shell.isJavaVersionAtLeast(22) && !securityManagerEnabled) {
115+
// This is the behaviour that breaks Hadoop authorization
116+
assertNull(childSubject);
117+
} else {
118+
assertEquals(parentSubject, childSubject);
119+
}
120+
}
121+
122+
@Test
123+
public void testThreadRunnable() {
124+
Subject parentSubject = new Subject();
125+
childSubject = null;
126+
127+
SubjectUtil.callAs(parentSubject, new Callable<Void>() {
128+
public Void call() throws InterruptedException {
129+
Runnable r = new Runnable() {
130+
@Override
131+
public void run() {
132+
childSubject = SubjectUtil.current();
133+
}
134+
};
135+
136+
Thread t = new Thread(r);
137+
t.start();
138+
t.join(1000);
139+
return (Void) null;
140+
}
141+
});
142+
143+
boolean securityManagerEnabled = true;
144+
try {
145+
SecurityManager sm = System.getSecurityManager();
146+
System.setSecurityManager(sm);
147+
} catch (UnsupportedOperationException e) {
148+
// JDK24+ always throws this
149+
securityManagerEnabled = false;
150+
} catch (Throwable t) {
151+
// don't care
152+
}
153+
154+
if (Shell.isJavaVersionAtLeast(22) && !securityManagerEnabled) {
155+
// This is the behaviour that breaks Hadoop authorization
156+
assertNull(childSubject);
157+
} else {
158+
assertEquals(parentSubject, childSubject);
159+
}
160+
}
161+
162+
}

0 commit comments

Comments
 (0)